From 9694bb8cac12969300692dac5a1cf7aa4e3a46cd Mon Sep 17 00:00:00 2001 From: drbrain Date: Thu, 29 Nov 2012 06:52:18 +0000 Subject: * lib/rubygems*: Updated to RubyGems 2.0 * test/rubygems*: ditto. * common.mk (prelude): Updated for RubyGems 2.0 source rearrangement. * tool/change_maker.rb: Allow invalid UTF-8 characters in source files. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@37976 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- lib/rubygems/available_set.rb | 95 ++ lib/rubygems/builder.rb | 99 -- lib/rubygems/command.rb | 126 ++- lib/rubygems/command_manager.rb | 107 +- lib/rubygems/commands/build_command.rb | 28 +- lib/rubygems/commands/cert_command.rb | 256 +++-- lib/rubygems/commands/check_command.rb | 53 +- lib/rubygems/commands/cleanup_command.rb | 7 + lib/rubygems/commands/contents_command.rb | 40 +- lib/rubygems/commands/dependency_command.rb | 11 +- lib/rubygems/commands/environment_command.rb | 21 +- lib/rubygems/commands/fetch_command.rb | 19 +- lib/rubygems/commands/generate_index_command.rb | 54 +- lib/rubygems/commands/help_command.rb | 2 +- lib/rubygems/commands/install_command.rb | 105 +- lib/rubygems/commands/list_command.rb | 10 +- lib/rubygems/commands/lock_command.rb | 2 +- lib/rubygems/commands/mirror_command.rb | 17 + lib/rubygems/commands/outdated_command.rb | 9 +- lib/rubygems/commands/owner_command.rb | 18 +- lib/rubygems/commands/pristine_command.rb | 21 +- lib/rubygems/commands/push_command.rb | 13 +- lib/rubygems/commands/query_command.rb | 70 +- lib/rubygems/commands/rdoc_command.rb | 51 +- lib/rubygems/commands/search_command.rb | 22 +- lib/rubygems/commands/server_command.rb | 2 +- lib/rubygems/commands/setup_command.rb | 145 ++- lib/rubygems/commands/sources_command.rb | 32 +- lib/rubygems/commands/specification_command.rb | 24 +- lib/rubygems/commands/uninstall_command.rb | 31 +- lib/rubygems/commands/unpack_command.rb | 10 +- lib/rubygems/commands/update_command.rb | 58 +- lib/rubygems/commands/yank_command.rb | 98 ++ lib/rubygems/compatibility.rb | 51 + lib/rubygems/config_file.rb | 136 ++- lib/rubygems/core_ext/kernel_gem.rb | 53 + lib/rubygems/core_ext/kernel_require.rb | 119 +++ lib/rubygems/custom_require.rb | 69 -- lib/rubygems/defaults.rb | 24 +- lib/rubygems/dependency.rb | 71 +- lib/rubygems/dependency_installer.rb | 226 +++-- lib/rubygems/dependency_list.rb | 30 +- lib/rubygems/dependency_resolver.rb | 562 +++++++++++ lib/rubygems/deprecate.rb | 80 +- lib/rubygems/doc_manager.rb | 243 ----- lib/rubygems/errors.rb | 101 +- lib/rubygems/exceptions.rb | 32 +- lib/rubygems/ext/builder.rb | 26 +- lib/rubygems/ext/configure_builder.rb | 4 +- lib/rubygems/ext/ext_conf_builder.rb | 4 +- lib/rubygems/ext/rake_builder.rb | 4 +- lib/rubygems/format.rb | 82 -- lib/rubygems/gem_openssl.rb | 90 -- lib/rubygems/gem_path_searcher.rb | 172 ---- lib/rubygems/gem_runner.rb | 19 +- lib/rubygems/gemcutter_utilities.rb | 29 +- lib/rubygems/indexer.rb | 168 +-- lib/rubygems/install_message.rb | 12 + lib/rubygems/install_update_options.rb | 74 +- lib/rubygems/installer.rb | 372 ++++--- lib/rubygems/installer_test_case.rb | 90 +- lib/rubygems/mock_gem_ui.rb | 17 + lib/rubygems/name_tuple.rb | 110 ++ lib/rubygems/old_format.rb | 153 --- lib/rubygems/package.rb | 556 +++++++++- lib/rubygems/package/digest_io.rb | 64 ++ lib/rubygems/package/f_sync_dir.rb | 23 - lib/rubygems/package/old.rb | 147 +++ lib/rubygems/package/tar_header.rb | 73 +- lib/rubygems/package/tar_input.rb | 235 ----- lib/rubygems/package/tar_output.rb | 146 --- lib/rubygems/package/tar_reader.rb | 23 +- lib/rubygems/package/tar_writer.rb | 70 +- lib/rubygems/package_task.rb | 7 +- lib/rubygems/path_support.rb | 21 +- lib/rubygems/platform.rb | 45 +- lib/rubygems/rdoc.rb | 316 ++++++ lib/rubygems/remote_fetcher.rb | 171 +++- lib/rubygems/request_set.rb | 182 ++++ lib/rubygems/require_paths_builder.rb | 18 - lib/rubygems/requirement.rb | 87 +- lib/rubygems/security.rb | 850 ++++++---------- lib/rubygems/security/policies.rb | 115 +++ lib/rubygems/security/policy.rb | 227 +++++ lib/rubygems/security/signer.rb | 136 +++ lib/rubygems/security/trust_dir.rb | 104 ++ lib/rubygems/server.rb | 100 +- lib/rubygems/source.rb | 144 +++ lib/rubygems/source_index.rb | 406 -------- lib/rubygems/source_list.rb | 87 ++ lib/rubygems/source_local.rb | 92 ++ lib/rubygems/source_specific_file.rb | 28 + lib/rubygems/spec_fetcher.rb | 298 ++---- lib/rubygems/specification.rb | 1066 ++++++++++++++------ lib/rubygems/ssl_certs/AddTrustExternalCARoot.pem | 90 ++ ...t_net-Secure-Server-Certification-Authority.pem | 90 ++ ...lass3PublicPrimaryCertificationAuthority-G2.pem | 57 ++ lib/rubygems/syck_hack.rb | 2 + lib/rubygems/test_case.rb | 245 ++++- lib/rubygems/test_utilities.rb | 30 +- lib/rubygems/uninstaller.rb | 82 +- lib/rubygems/user_interaction.rb | 10 + lib/rubygems/validator.rb | 73 +- lib/rubygems/version.rb | 23 +- 104 files changed, 7053 insertions(+), 4365 deletions(-) create mode 100644 lib/rubygems/available_set.rb delete mode 100644 lib/rubygems/builder.rb create mode 100644 lib/rubygems/commands/mirror_command.rb create mode 100644 lib/rubygems/commands/yank_command.rb create mode 100644 lib/rubygems/compatibility.rb create mode 100644 lib/rubygems/core_ext/kernel_gem.rb create mode 100755 lib/rubygems/core_ext/kernel_require.rb delete mode 100644 lib/rubygems/custom_require.rb create mode 100644 lib/rubygems/dependency_resolver.rb delete mode 100644 lib/rubygems/doc_manager.rb delete mode 100644 lib/rubygems/format.rb delete mode 100644 lib/rubygems/gem_openssl.rb delete mode 100644 lib/rubygems/gem_path_searcher.rb create mode 100644 lib/rubygems/install_message.rb create mode 100644 lib/rubygems/name_tuple.rb delete mode 100644 lib/rubygems/old_format.rb create mode 100644 lib/rubygems/package/digest_io.rb delete mode 100644 lib/rubygems/package/f_sync_dir.rb create mode 100644 lib/rubygems/package/old.rb delete mode 100644 lib/rubygems/package/tar_input.rb delete mode 100644 lib/rubygems/package/tar_output.rb create mode 100644 lib/rubygems/rdoc.rb create mode 100644 lib/rubygems/request_set.rb delete mode 100644 lib/rubygems/require_paths_builder.rb create mode 100644 lib/rubygems/security/policies.rb create mode 100644 lib/rubygems/security/policy.rb create mode 100644 lib/rubygems/security/signer.rb create mode 100644 lib/rubygems/security/trust_dir.rb create mode 100644 lib/rubygems/source.rb delete mode 100644 lib/rubygems/source_index.rb create mode 100644 lib/rubygems/source_list.rb create mode 100644 lib/rubygems/source_local.rb create mode 100644 lib/rubygems/source_specific_file.rb create mode 100644 lib/rubygems/ssl_certs/AddTrustExternalCARoot.pem create mode 100644 lib/rubygems/ssl_certs/Entrust_net-Secure-Server-Certification-Authority.pem create mode 100644 lib/rubygems/ssl_certs/VerisignClass3PublicPrimaryCertificationAuthority-G2.pem (limited to 'lib/rubygems') diff --git a/lib/rubygems/available_set.rb b/lib/rubygems/available_set.rb new file mode 100644 index 0000000000..cd6df1ddc7 --- /dev/null +++ b/lib/rubygems/available_set.rb @@ -0,0 +1,95 @@ +module Gem + class AvailableSet + Tuple = Struct.new(:spec, :source) + + def initialize + @set = [] + @sorted = nil + end + + attr_reader :set + + def add(spec, source) + @set << Tuple.new(spec, source) + @sorted = nil + self + end + + def <<(o) + case o + when AvailableSet + s = o.set + when Array + s = o.map do |sp,so| + if !sp.kind_of?(Specification) or !so.kind_of?(Source) + raise TypeError, "Array must be in [[spec, source], ...] form" + end + + Tuple.new(sp,so) + end + else + raise TypeError, "Must be an AvailableSet" + end + + @set += s + @sorted = nil + + self + end + + def empty? + @set.empty? + end + + def all_specs + @set.map { |t| t.spec } + end + + def match_platform! + @set.reject! { |t| !Gem::Platform.match(t.spec.platform) } + @sorted = nil + self + end + + def sorted + @sorted ||= @set.sort do |a,b| + i = b.spec <=> a.spec + i != 0 ? i : (a.source <=> b.source) + end + end + + def size + @set.size + end + + def source_for(spec) + f = @set.find { |t| t.spec == spec } + f.source + end + + def pick_best! + return self if empty? + + @set = [sorted.first] + @sorted = nil + self + end + + def remove_installed!(dep) + @set.reject! do |t| + # already locally installed + Gem::Specification.any? do |installed_spec| + dep.name == installed_spec.name and + dep.requirement.satisfied_by? installed_spec.version + end + end + + @sorted = nil + self + end + + def inject_into_list(dep_list) + @set.each { |t| dep_list.add t.spec } + end + end +end diff --git a/lib/rubygems/builder.rb b/lib/rubygems/builder.rb deleted file mode 100644 index 25e8fc8321..0000000000 --- a/lib/rubygems/builder.rb +++ /dev/null @@ -1,99 +0,0 @@ -#-- -# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. -# All rights reserved. -# See LICENSE.txt for permissions. -#++ - -require 'rubygems' -require 'rubygems/user_interaction' - -Gem.load_yaml - -require 'rubygems/package' - -## -# The Builder class processes RubyGem specification files -# to produce a .gem file. - -class Gem::Builder - - include Gem::UserInteraction - - ## - # Constructs a builder instance for the provided specification - # - # spec:: [Gem::Specification] The specification instance - - def initialize(spec) - @spec = spec - end - - ## - # Builds the gem from the specification. Returns the name of the file - # written. - - def build(skip_validation=false) - @spec.mark_version - @spec.validate unless skip_validation - @signer = sign - write_package - say success if Gem.configuration.verbose - File.basename @spec.cache_file - end - - def success - <<-EOM - Successfully built RubyGem - Name: #{@spec.name} - Version: #{@spec.version} - File: #{File.basename @spec.cache_file} -EOM - end - - private - - ## - # If the signing key was specified, then load the file, and swap to the - # public key (TODO: we should probably just omit the signing key in favor of - # the signing certificate, but that's for the future, also the signature - # algorithm should be configurable) - - def sign - signer = nil - - if @spec.respond_to?(:signing_key) and @spec.signing_key then - require 'rubygems/security' - - signer = Gem::Security::Signer.new @spec.signing_key, @spec.cert_chain - @spec.signing_key = nil - @spec.cert_chain = signer.cert_chain.map { |cert| cert.to_s } - end - - signer - end - - def write_package - file_name = File.basename @spec.cache_file - open file_name, 'wb' do |gem_io| - Gem::Package.open gem_io, 'w', @signer do |pkg| - yaml = @spec.to_yaml - pkg.metadata = yaml - - @spec.files.each do |file| - next if File.directory?(file) - next if file == file_name # Don't add gem onto itself - - stat = File.stat(file) - mode = stat.mode & 0777 - size = stat.size - - pkg.add_file_simple file, mode, size do |tar_io| - tar_io.write open(file, "rb") { |f| f.read } - end - end - end - end - end - -end - diff --git a/lib/rubygems/command.rb b/lib/rubygems/command.rb index 49253ef56b..69cbf3c269 100644 --- a/lib/rubygems/command.rb +++ b/lib/rubygems/command.rb @@ -9,7 +9,7 @@ require 'rubygems/user_interaction' ## # Base class for all Gem commands. When creating a new gem command, define -# #new, #execute, #arguments, #defaults_str, #description and #usage +# #initialize, #execute, #arguments, #defaults_str, #description and #usage # (as appropriate). See the above mentioned methods for details. # # A very good example to look at is Gem::Commands::ContentsCommand @@ -150,8 +150,9 @@ class Gem::Command def show_lookup_failure(gem_name, version, errors, domain) if errors and !errors.empty? - alert_error "Could not find a valid gem '#{gem_name}' (#{version}), here is why:" - errors.each { |x| say " #{x.wordy}" } + msg = "Could not find a valid gem '#{gem_name}' (#{version}), here is why:\n" + errors.each { |x| msg << " #{x.wordy}\n" } + alert_error msg else alert_error "Could not find a valid gem '#{gem_name}' (#{version}) in any repository" end @@ -179,6 +180,15 @@ class Gem::Command args.select { |arg| arg !~ /^-/ } end + ## + # Get all [gem, version] from the command line. + # + # An argument in the form gem:ver is pull apart into the gen name and version, + # respectively. + def get_all_gem_names_and_versions + get_all_gem_names.map { |name| name.split(":", 2) } + end + ## # Get a single gem name from the command line. Fail if there is no gem name # or if there is more than one gem name given. @@ -268,8 +278,18 @@ class Gem::Command # Invoke the command with the given list of arguments. def invoke(*args) + invoke_with_build_args args, nil + end + + ## + # Invoke the command with the given list of normal arguments + # and additional build arguments. + + def invoke_with_build_args(args, build_args) handle_options args + options[:build_args] = build_args + if options[:help] then show_help elsif @when_invoked then @@ -344,7 +364,7 @@ class Gem::Command def handle_options(args) args = add_extra_args(args) - @options = @defaults.clone + @options = Marshal.load Marshal.dump @defaults # deep copy parser.parse!(args) @options[:args] = args end @@ -372,18 +392,23 @@ class Gem::Command private - ## - # Create on demand parser. + def add_parser_description # :nodoc: + return unless description - def parser - create_option_parser if @parser.nil? - @parser - end + formatted = description.split("\n\n").map do |chunk| + wrap chunk, 80 - 4 + end.join "\n" - def create_option_parser - @parser = OptionParser.new + @parser.separator nil + @parser.separator " Description:" + formatted.split("\n").each do |line| + @parser.separator " #{line.rstrip}" + end + end + def add_parser_options # :nodoc: @parser.separator nil + regular_options = @option_groups.delete :options configure_options "", regular_options @@ -392,45 +417,56 @@ class Gem::Command @parser.separator nil configure_options group_name, option_list end + end - @parser.separator nil - configure_options "Common", Gem::Command.common_options + ## + # Adds a section with +title+ and +content+ to the parser help view. Used + # for adding command arguments and default arguments. - unless arguments.empty? - @parser.separator nil - @parser.separator " Arguments:" - arguments.split(/\n/).each do |arg_desc| - @parser.separator " #{arg_desc}" - end - end + def add_parser_run_info title, content + return if content.empty? - if @summary then - @parser.separator nil - @parser.separator " Summary:" - wrap(@summary, 80 - 4).split("\n").each do |line| - @parser.separator " #{line.strip}" - end + @parser.separator nil + @parser.separator " #{title}:" + content.split(/\n/).each do |line| + @parser.separator " #{line}" end + end - if description then - formatted = description.split("\n\n").map do |chunk| - wrap chunk, 80 - 4 - end.join "\n" + def add_parser_summary # :nodoc: + return unless @summary - @parser.separator nil - @parser.separator " Description:" - formatted.split("\n").each do |line| - @parser.separator " #{line.rstrip}" - end + @parser.separator nil + @parser.separator " Summary:" + wrap(@summary, 80 - 4).split("\n").each do |line| + @parser.separator " #{line.strip}" end + end - unless defaults_str.empty? - @parser.separator nil - @parser.separator " Defaults:" - defaults_str.split(/\n/).each do |line| - @parser.separator " #{line}" - end - end + ## + # Create on demand parser. + + def parser + create_option_parser if @parser.nil? + @parser + end + + ## + # Creates an option parser and fills it in with the help info for the + # command. + + def create_option_parser + @parser = OptionParser.new + + add_parser_options + + @parser.separator nil + configure_options "Common", Gem::Command.common_options + + add_parser_run_info "Arguments", arguments + add_parser_summary + add_parser_description + add_parser_run_info "Defaults", defaults_str end def configure_options(header, option_list) @@ -521,7 +557,7 @@ basic help message containing pointers to more information. http://localhost:8808/ with info about installed gems Further information: - http://rubygems.rubyforge.org + http://guides.rubygems.org HELP # :startdoc: @@ -529,7 +565,7 @@ basic help message containing pointers to more information. end ## -# This is where Commands will be placed in the namespace +# \Commands will be placed in this namespace module Gem::Commands end diff --git a/lib/rubygems/command_manager.rb b/lib/rubygems/command_manager.rb index 9edd550136..e39c509ef9 100644 --- a/lib/rubygems/command_manager.rb +++ b/lib/rubygems/command_manager.rb @@ -18,12 +18,15 @@ require 'rubygems/user_interaction' # # file rubygems_plugin.rb # require 'rubygems/command_manager' # +# Gem::CommandManager.instance.register_command :edit +# +# You should put the implementation of your command in rubygems/commands. +# +# # file rubygems/commands/edit_command.rb # class Gem::Commands::EditCommand < Gem::Command # # ... # end # -# Gem::CommandManager.instance.register_command :edit -# # See Gem::Command for instructions on writing gem commands. class Gem::CommandManager @@ -37,6 +40,14 @@ class Gem::CommandManager @command_manager ||= new end + ## + # Returns self. Allows a CommandManager instance to stand + # in for the class itself. + + def instance + self + end + ## # Reset the authoritative instance of the command manager. @@ -63,6 +74,7 @@ class Gem::CommandManager register_command :install register_command :list register_command :lock + register_command :mirror register_command :outdated register_command :owner register_command :pristine @@ -78,13 +90,14 @@ class Gem::CommandManager register_command :unpack register_command :update register_command :which + register_command :yank end ## # Register the Symbol +command+ as a gem command. - def register_command(command) - @commands[command] = false + def register_command(command, obj=false) + @commands[command] = obj end ## @@ -95,7 +108,7 @@ class Gem::CommandManager end ## - # Return the registered command from the command name. + # Returns a Command instance for +command_name+ def [](command_name) command_name = command_name.intern @@ -104,56 +117,69 @@ class Gem::CommandManager end ## - # Return a sorted list of all command names (as strings). + # Return a sorted list of all command names as strings. def command_names @commands.keys.collect {|key| key.to_s}.sort end ## - # Run the config specified by +args+. + # Run the command specified by +args+. - def run(args) - process_args(args) + def run(args, build_args=nil) + process_args(args, build_args) rescue StandardError, Timeout::Error => ex alert_error "While executing gem ... (#{ex.class})\n #{ex.to_s}" - ui.errs.puts "\t#{ex.backtrace.join "\n\t"}" if - Gem.configuration.backtrace + ui.backtrace ex + + if Gem.configuration.really_verbose and \ + ex.kind_of?(Gem::Exception) and ex.source_exception + e = ex.source_exception + + ui.errs.puts "Because of: (#{e.class})\n #{e.to_s}" + ui.backtrace e + end + terminate_interaction(1) rescue Interrupt alert_error "Interrupted" terminate_interaction(1) end - def process_args(args) + def process_args(args, build_args=nil) args = args.to_str.split(/\s+/) if args.respond_to?(:to_str) - if args.size == 0 + + if args.empty? then say Gem::Command::HELP - terminate_interaction(1) + terminate_interaction 1 end - case args[0] - when '-h', '--help' + + case args.first + when '-h', '--help' then say Gem::Command::HELP - terminate_interaction(0) - when '-v', '--version' + terminate_interaction 0 + when '-v', '--version' then say Gem::VERSION - terminate_interaction(0) - when /^-/ - alert_error "Invalid option: #{args[0]}. See 'gem --help'." - terminate_interaction(1) + terminate_interaction 0 + when /^-/ then + alert_error "Invalid option: #{args.first}. See 'gem --help'." + terminate_interaction 1 else cmd_name = args.shift.downcase - cmd = find_command(cmd_name) - cmd.invoke(*args) + cmd = find_command cmd_name + cmd.invoke_with_build_args args, build_args end end def find_command(cmd_name) possibilities = find_command_possibilities cmd_name + if possibilities.size > 1 then - raise "Ambiguous command #{cmd_name} matches [#{possibilities.join(', ')}]" - elsif possibilities.size < 1 then - raise "Unknown command #{cmd_name}" + raise Gem::CommandLineError, + "Ambiguous command #{cmd_name} " \ + "matches [#{possibilities.join(', ')}]" + elsif possibilities.empty? then + raise Gem::CommandLineError, "Unknown command #{cmd_name}" end self[possibilities.first] @@ -162,7 +188,11 @@ class Gem::CommandManager def find_command_possibilities(cmd_name) len = cmd_name.length - command_names.select { |n| cmd_name == n[0, len] } + found = command_names.select { |name| cmd_name == name[0, len] } + + exact = found.find { |name| name == cmd_name } + + exact ? [exact] : found end private @@ -170,23 +200,20 @@ class Gem::CommandManager def load_and_instantiate(command_name) command_name = command_name.to_s const_name = command_name.capitalize.gsub(/_(.)/) { $1.upcase } << "Command" - commands = Gem::Commands - retried = false + load_error = nil begin - commands.const_get(const_name).new - rescue NameError - raise if retried - - retried = true begin require "rubygems/commands/#{command_name}_command" - rescue Exception => e - alert_error "Loading command: #{command_name} (#{e.class})\n #{e}" - ui.errs.puts "\t#{e.backtrace.join "\n\t"}" if - Gem.configuration.backtrace + rescue LoadError => e + load_error = e end - retry + Gem::Commands.const_get(const_name).new + rescue Exception => e + e = load_error if load_error + + alert_error "Loading command: #{command_name} (#{e.class})\n\t#{e}" + ui.backtrace e end end diff --git a/lib/rubygems/commands/build_command.rb b/lib/rubygems/commands/build_command.rb index 36a6fe48f2..64563ed3db 100644 --- a/lib/rubygems/commands/build_command.rb +++ b/lib/rubygems/commands/build_command.rb @@ -1,5 +1,5 @@ require 'rubygems/command' -require 'rubygems/builder' +require 'rubygems/package' class Gem::Commands::BuildCommand < Gem::Command @@ -22,11 +22,11 @@ class Gem::Commands::BuildCommand < Gem::Command def execute gemspec = get_one_gem_name - if File.exist? gemspec - spec = load_gemspec gemspec + if File.exist? gemspec then + spec = Gem::Specification.load gemspec if spec then - Gem::Builder.new(spec).build options[:force] + Gem::Package.build spec, options[:force] else alert_error "Error loading gemspec. Aborting." terminate_interaction 1 @@ -37,23 +37,5 @@ class Gem::Commands::BuildCommand < Gem::Command end end - def load_gemspec filename - if yaml?(filename) - open(filename) do |f| - begin - Gem::Specification.from_yaml(f) - rescue Gem::EndOfYAMLException - nil - end - end - else - Gem::Specification.load(filename) # can return nil - end - end - - def yaml?(filename) - line = open(filename) { |f| line = f.gets } - result = line =~ %r{!ruby/object:Gem::Specification} - result - end end + diff --git a/lib/rubygems/commands/cert_command.rb b/lib/rubygems/commands/cert_command.rb index b416b3863d..371ab403c6 100644 --- a/lib/rubygems/commands/cert_command.rb +++ b/lib/rubygems/commands/cert_command.rb @@ -4,82 +4,224 @@ require 'rubygems/security' class Gem::Commands::CertCommand < Gem::Command def initialize - super 'cert', 'Manage RubyGems certificates and signing settings' - - add_option('-a', '--add CERT', - 'Add a trusted certificate.') do |value, options| - cert = OpenSSL::X509::Certificate.new(File.read(value)) - Gem::Security.add_trusted_cert(cert) - say "Added '#{cert.subject.to_s}'" - end - - add_option('-l', '--list', - 'List trusted certificates.') do |value, options| - glob_str = File::join(Gem::Security::OPT[:trust_dir], '*.pem') - Dir::glob(glob_str) do |path| - begin - cert = OpenSSL::X509::Certificate.new(File.read(path)) - # this could probably be formatted more gracefully - say cert.subject.to_s - rescue OpenSSL::X509::CertificateError - next - end + super 'cert', 'Manage RubyGems certificates and signing settings', + :add => [], :remove => [], :list => [], :build => [], :sign => [] + + OptionParser.accept OpenSSL::X509::Certificate do |certificate| + begin + OpenSSL::X509::Certificate.new File.read certificate + rescue Errno::ENOENT + raise OptionParser::InvalidArgument, "#{certificate}: does not exist" + rescue OpenSSL::X509::CertificateError + raise OptionParser::InvalidArgument, + "#{certificate}: invalid X509 certificate" end end - add_option('-r', '--remove STRING', - 'Remove trusted certificates containing', - 'STRING.') do |value, options| - trust_dir = Gem::Security::OPT[:trust_dir] - glob_str = File::join(trust_dir, '*.pem') - - Dir::glob(glob_str) do |path| - begin - cert = OpenSSL::X509::Certificate.new(File.read(path)) - if cert.subject.to_s.downcase.index(value) - say "Removed '#{cert.subject.to_s}'" - File.unlink(path) - end - rescue OpenSSL::X509::CertificateError - next - end + OptionParser.accept OpenSSL::PKey::RSA do |key_file| + begin + key = OpenSSL::PKey::RSA.new File.read key_file + rescue Errno::ENOENT + raise OptionParser::InvalidArgument, "#{key_file}: does not exist" + rescue OpenSSL::PKey::RSAError + raise OptionParser::InvalidArgument, "#{key_file}: invalid RSA key" end + + raise OptionParser::InvalidArgument, + "#{key_file}: private key not found" unless key.private? + + key + end + + add_option('-a', '--add CERT', OpenSSL::X509::Certificate, + 'Add a trusted certificate.') do |cert, options| + options[:add] << cert + 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 |value, options| - vals = Gem::Security.build_self_signed_cert(value) - FileUtils.chmod 0600, vals[:key_path] - say "Public Cert: #{vals[:cert_path]}" - say "Private Key: #{vals[:key_path]}" - say "Don't forget to move the key file to somewhere private..." + 'certificate for EMAIL_ADDR') do |email_address, options| + options[:build] << email_address end - add_option('-C', '--certificate CERT', - 'Certificate for --sign command.') do |value, options| - cert = OpenSSL::X509::Certificate.new(File.read(value)) + add_option('-C', '--certificate CERT', OpenSSL::X509::Certificate, + 'Signing certificate for --sign') do |cert, options| options[:issuer_cert] = cert end - add_option('-K', '--private-key KEY', - 'Private key for --sign command.') do |value, options| - key = OpenSSL::PKey::RSA.new(File.read(value)) - options[:issuer_key] = key + add_option('-K', '--private-key KEY', OpenSSL::PKey::RSA, + 'Key for --sign or --build') do |key, options| + options[:key] = key end - add_option('-s', '--sign NEWCERT', - 'Sign a certificate with my key and', - 'certificate.') do |value, options| - cert = OpenSSL::X509::Certificate.new(File.read(value)) - my_cert = options[:issuer_cert] - my_key = options[:issuer_key] - cert = Gem::Security.sign_cert(cert, my_key, my_cert) - File.open(value, 'wb') { |file| file.write(cert.to_pem) } + add_option('-s', '--sign CERT', + 'Signs CERT with the key from -K', + 'and the certificate from -C') do |cert_file, options| + raise OptionParser::InvalidArgument, "#{cert_file}: does not exist" unless + File.file? cert_file + + options[:sign] << cert_file end end def execute + options[:add].each do |certificate| + Gem::Security.trust_dir.trust_cert certificate + + say "Added '#{certificate.subject}'" + end + + options[:remove].each do |filter| + certificates_matching filter do |certificate, path| + FileUtils.rm path + say "Removed '#{certificate.subject}'" + end + end + + options[:list].each do |filter| + certificates_matching filter do |certificate, _| + # this could probably be formatted more gracefully + say certificate.subject.to_s + end + end + + options[:build].each do |name| + build name + end + + unless options[:sign].empty? then + load_default_cert unless options[:issuer_cert] + load_default_key unless options[:key] + end + + options[:sign].each do |cert_file| + sign cert_file + end + end + + def build name + key = options[:key] || Gem::Security.create_key + + cert = Gem::Security.create_cert_email name, key + + key_path = Gem::Security.write key, "gem-private_key.pem" + cert_path = Gem::Security.write cert, "gem-public_cert.pem" + + say "Certificate: #{cert_path}" + say "Private Key: #{key_path}" + say "Don't forget to move the key file to somewhere private!" + 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 load_default_cert + cert_file = File.join Gem.user_home, 'gem-public_cert.pem' + 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.user_home, 'gem-private_key.pem' + key = File.read key_file + options[:key] = OpenSSL::PKey::RSA.new key + rescue Errno::ENOENT + alert_error \ + "--private-key not specified and ~/.gem/gem-private_key.pem does not exist" + + terminate_interaction 1 + rescue OpenSSL::PKey::RSAError + alert_error \ + "--private-key not specified and ~/.gem/gem-private_key.pem is not valid" + + terminate_interaction 1 + end + + def sign cert_file + cert = File.read cert_file + cert = OpenSSL::X509::Certificate.new cert + + permissions = File.stat(cert_file).mode & 0777 + + 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 end diff --git a/lib/rubygems/commands/check_command.rb b/lib/rubygems/commands/check_command.rb index 5a1bfd4f12..4bdfcfa645 100644 --- a/lib/rubygems/commands/check_command.rb +++ b/lib/rubygems/commands/check_command.rb @@ -8,13 +8,7 @@ class Gem::Commands::CheckCommand < Gem::Command def initialize super 'check', 'Check installed gems', - :verify => false, :alien => false - - add_option( '--verify FILE', - 'Verify gem file against its internal', - 'checksum') do |value, options| - options[:verify] = value - end + :alien => true add_option('-a', '--alien', "Report 'unmanaged' or rogue files in the", "gem repository") do |value, options| @@ -25,40 +19,21 @@ class Gem::Commands::CheckCommand < Gem::Command end def execute - if options[:alien] - say "Performing the 'alien' operation" - say - gems = get_all_gem_names rescue [] - Gem::Validator.new.alien(gems).sort.each do |key, val| - unless val.empty? then - say "#{key} has #{val.size} problems" - val.each do |error_entry| - say " #{error_entry.path}:" - say " #{error_entry.problem}" - end - else - say "#{key} is error-free" if Gem.configuration.verbose + say "Checking gems..." + say + gems = get_all_gem_names rescue [] + + Gem::Validator.new.alien(gems).sort.each do |key, val| + unless val.empty? then + say "#{key} has #{val.size} problems" + val.each do |error_entry| + say " #{error_entry.path}:" + say " #{error_entry.problem}" end - say - end - end - - if options[:verify] - gem_name = options[:verify] - unless gem_name - alert_error "Must specify a .gem file with --verify NAME" - return - end - unless File.exist?(gem_name) - alert_error "Unknown file: #{gem_name}." - return - end - say "Verifying gem: '#{gem_name}'" - begin - Gem::Validator.new.verify_gem_file(gem_name) - rescue Exception - alert_error "#{gem_name} is invalid." + else + say "#{key} is error-free" if Gem.configuration.verbose end + say end end diff --git a/lib/rubygems/commands/cleanup_command.rb b/lib/rubygems/commands/cleanup_command.rb index 124c4c203a..dc919e5570 100644 --- a/lib/rubygems/commands/cleanup_command.rb +++ b/lib/rubygems/commands/cleanup_command.rb @@ -26,6 +26,9 @@ class Gem::Commands::CleanupCommand < Gem::Command <<-EOF The cleanup command removes old gems from GEM_HOME. If an older version is installed elsewhere in GEM_PATH the cleanup command won't touch it. + +Older gems that are required to satisify the dependencies of gems +are not removed. EOF end @@ -56,6 +59,8 @@ installed elsewhere in GEM_PATH the cleanup command won't touch it. primary_gems[spec.name].version != spec.version } + full = Gem::DependencyList.from_specs + deplist = Gem::DependencyList.new gems_to_cleanup.uniq.each do |spec| deplist.add spec end @@ -64,6 +69,8 @@ installed elsewhere in GEM_PATH the cleanup command won't touch it. original_path = Gem.path deps.each do |spec| + next unless full.ok_to_remove?(spec.full_name) + if options[:dryrun] then say "Dry Run Mode: Would uninstall #{spec.full_name}" else diff --git a/lib/rubygems/commands/contents_command.rb b/lib/rubygems/commands/contents_command.rb index e483484615..404c6745bd 100644 --- a/lib/rubygems/commands/contents_command.rb +++ b/lib/rubygems/commands/contents_command.rb @@ -1,3 +1,4 @@ +require 'English' require 'rubygems/command' require 'rubygems/version_option' @@ -80,19 +81,36 @@ class Gem::Commands::ContentsCommand < Gem::Command terminate_interaction 1 if gem_names.length == 1 end - gem_path = spec.full_gem_path - extra = "/{#{spec.require_paths.join ','}}" if options[:lib_only] - glob = "#{gem_path}#{extra}/**/*" - files = Dir[glob] - - gem_path = File.join gem_path, '' # add trailing / if missing - - files.sort.each do |file| - next if File.directory? file + if spec.default_gem? + files = spec.files.map do |file| + case file + when /\A#{spec.bindir}\// + [Gem::ConfigMap[:bindir], $POSTMATCH] + when /\.so\z/ + [Gem::ConfigMap[:archdir], file] + else + [Gem::ConfigMap[:rubylibdir], file] + end + end + else + gem_path = spec.full_gem_path + extra = "/{#{spec.require_paths.join ','}}" if options[:lib_only] + glob = "#{gem_path}#{extra}/**/*" + prefix_re = /#{Regexp.escape(gem_path)}\// + files = Dir[glob].map do |file| + [gem_path, file.sub(prefix_re, "")] + end + end - file = file.sub gem_path, '' unless options[:prefix] + files.sort.each do |prefix, basename| + absolute_path = File.join(prefix, basename) + next if File.directory? absolute_path - say file + if options[:prefix] + say absolute_path + else + say basename + end end end end diff --git a/lib/rubygems/commands/dependency_command.rb b/lib/rubygems/commands/dependency_command.rb index 67cbbc1d5e..4690b13a94 100644 --- a/lib/rubygems/commands/dependency_command.rb +++ b/lib/rubygems/commands/dependency_command.rb @@ -71,14 +71,9 @@ class Gem::Commands::DependencyCommand < Gem::Command if remote? and not options[:reverse_dependencies] then fetcher = Gem::SpecFetcher.fetcher - # REFACTOR: fetcher.find_specs_matching => specs - specs_and_sources = fetcher.find_matching(dependency, - dependency.specific?, true, - dependency.prerelease?) - - specs.concat specs_and_sources.map { |spec_tuple, source_uri| - fetcher.fetch_spec spec_tuple, URI.parse(source_uri) - } + ss, _ = fetcher.spec_for_dependency dependency + + ss.each { |s,o| specs << s } end if specs.empty? then diff --git a/lib/rubygems/commands/environment_command.rb b/lib/rubygems/commands/environment_command.rb index 9585c71250..40e71cf094 100644 --- a/lib/rubygems/commands/environment_command.rb +++ b/lib/rubygems/commands/environment_command.rb @@ -24,33 +24,38 @@ class Gem::Commands::EnvironmentCommand < Gem::Command 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 -~/.gemrc file for individual users and a /etc/gemrc for all users. A gemrc -is a YAML file with the following YAML keys: +Command line argument defaults and some RubyGems defaults can be set in a +~/.gemrc file for individual users and a /etc/gemrc 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 + :verbose: Verbosity of the gem command. false, true, and :really are the levels :update_sources: Enable/disable automatic updating of repository metadata :backtrace: Print backtrace when RubyGems encounters an error :gempath: The paths in which to look for gems - gem_command: A string containing arguments for the specified gem command + :disable_default_gem_server: Force specification of gem server host on push + : 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 +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 @@ -74,7 +79,7 @@ lib/rubygems/defaults/operating_system.rb when /^gempath/, /^path/, /^GEM_PATH/ then out << Gem.path.join(File::PATH_SEPARATOR) when /^remotesources/ then - out << Gem.sources.join("\n") + out << Gem.sources.to_a.join("\n") when /^platform/ then out << Gem.platforms.join(File::PATH_SEPARATOR) when nil then diff --git a/lib/rubygems/commands/fetch_command.rb b/lib/rubygems/commands/fetch_command.rb index e7c9cc9525..ec021359b6 100644 --- a/lib/rubygems/commands/fetch_command.rb +++ b/lib/rubygems/commands/fetch_command.rb @@ -34,7 +34,6 @@ class Gem::Commands::FetchCommand < Gem::Command def execute version = options[:version] || Gem::Requirement.default - all = Gem::Requirement.default != version platform = Gem.platforms.last gem_names = get_all_gem_names @@ -43,32 +42,20 @@ class Gem::Commands::FetchCommand < Gem::Command dep = Gem::Dependency.new gem_name, version dep.prerelease = options[:prerelease] - specs_and_sources, errors = - Gem::SpecFetcher.fetcher.fetch_with_errors(dep, all, true, - dep.prerelease?) - + specs_and_sources, errors = Gem::SpecFetcher.fetcher.spec_for_dependency dep if platform then filtered = specs_and_sources.select { |s,| s.platform == platform } specs_and_sources = filtered unless filtered.empty? end - spec, source_uri = specs_and_sources.sort_by { |s,| s.version }.last + spec, source = specs_and_sources.sort_by { |s,| s.version }.first if spec.nil? then show_lookup_failure gem_name, version, errors, options[:domain] next end - file = "#{spec.full_name}.gem" - remote_path = URI.parse(source_uri) + "gems/#{file}" - - fetch = Gem::RemoteFetcher.fetcher - - gem = fetch.fetch_path remote_path.to_s - - File.open file, "wb" do |f| - f.write gem - end + source.download spec say "Downloaded #{spec.full_name}" end diff --git a/lib/rubygems/commands/generate_index_command.rb b/lib/rubygems/commands/generate_index_command.rb index d4b4790649..a7db013caf 100644 --- a/lib/rubygems/commands/generate_index_command.rb +++ b/lib/rubygems/commands/generate_index_command.rb @@ -11,29 +11,16 @@ class Gem::Commands::GenerateIndexCommand < Gem::Command def initialize super 'generate_index', 'Generates the index files for a gem server directory', - :directory => '.', :build_legacy => true, :build_modern => true + :directory => '.', :build_modern => true add_option '-d', '--directory=DIRNAME', 'repository base dir containing gems subdir' do |dir, options| options[:directory] = File.expand_path dir end - add_option '--[no-]legacy', - 'Generate Marshal.4.8' do |value, options| - unless options[:build_modern] or value then - raise OptionParser::InvalidOption, 'no indicies will be built' - end - - options[:build_legacy] = value - end - add_option '--[no-]modern', - 'Generate indexes for RubyGems newer', - 'than 1.2.0' do |value, options| - unless options[:build_legacy] or value then - raise OptionParser::InvalidOption, 'no indicies will be built' - end - + 'Generate indexes for RubyGems', + '(always true)' do |value, options| options[:build_modern] = value end @@ -42,27 +29,10 @@ class Gem::Commands::GenerateIndexCommand < Gem::Command 'since the last update' do |value, options| options[:update] = value end - - add_option :RSS, '--rss-gems-host=GEM_HOST', - 'Host name where gems are served from,', - 'used for GUID and enclosure values' do |value, options| - options[:rss_gems_host] = value - end - - add_option :RSS, '--rss-host=HOST', - 'Host name for more gems information,', - 'used for RSS feed link' do |value, options| - options[:rss_host] = value - end - - add_option :RSS, '--rss-title=TITLE', - 'Set title for RSS feed' do |value, options| - options[:rss_title] = value - end end def defaults_str # :nodoc: - "--directory . --legacy --modern" + "--directory . --modern" end def description # :nodoc: @@ -85,25 +55,15 @@ When done, it will generate a set of files like this: prerelease_specs..gz # prerelease specs index quick/Marshal./.gemspec.rz # Marshal quick index file - # these files support legacy RubyGems - Marshal. - Marshal..Z # Marshal full index - -The .Z and .rz extension files are compressed with the inflate algorithm. +The .rz extension files are compressed with the inflate algorithm. The Marshal version number comes from ruby's Marshal::MAJOR_VERSION and Marshal::MINOR_VERSION constants. It is used to ensure compatibility. - -If --rss-host and --rss-gem-host are given an RSS feed will be generated at -index.rss containing gems released in the last two days. EOF end def execute - if options[:update] and - (options[:rss_host] or options[:rss_gems_host]) then - alert_error '--update not compatible with RSS generation' - terminate_interaction 1 - end + # This is always true becasue it's the only way now. + options[:build_modern] = true if not File.exist?(options[:directory]) or not File.directory?(options[:directory]) then diff --git a/lib/rubygems/commands/help_command.rb b/lib/rubygems/commands/help_command.rb index 20b52429b2..8e3d97edd3 100644 --- a/lib/rubygems/commands/help_command.rb +++ b/lib/rubygems/commands/help_command.rb @@ -37,7 +37,7 @@ Some examples of 'gem' usage. * Create a gem: - See http://rubygems.rubyforge.org/wiki/wiki.pl?CreateAGemInTenMinutes + See http://guides.rubygems.org/make-your-own-gem/ * See information about RubyGems: diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb index 003ba8601c..883526c2a5 100644 --- a/lib/rubygems/commands/install_command.rb +++ b/lib/rubygems/commands/install_command.rb @@ -1,10 +1,11 @@ require 'rubygems/command' -require 'rubygems/doc_manager' require 'rubygems/install_update_options' require 'rubygems/dependency_installer' require 'rubygems/local_remote_options' require 'rubygems/validator' require 'rubygems/version_option' +require 'rubygems/install_message' # must come before rdoc for messaging +require 'rubygems/rdoc' ## # Gem installer command line tool @@ -13,14 +14,14 @@ require 'rubygems/version_option' class Gem::Commands::InstallCommand < Gem::Command + attr_reader :installed_specs # :nodoc: + include Gem::VersionOption include Gem::LocalRemoteOptions include Gem::InstallUpdateOptions def initialize defaults = Gem::DependencyInstaller::DEFAULT_OPTIONS.merge({ - :generate_rdoc => true, - :generate_ri => true, :format_executable => false, :version => Gem::Requirement.default, }) @@ -32,6 +33,14 @@ class Gem::Commands::InstallCommand < Gem::Command add_platform_option add_version_option add_prerelease_option "to be installed. (Only for listed gems)" + + add_option(:"Install/Update", '-g', '--file FILE', + 'Read from a gem dependencies API file and', + 'install the listed gems') do |v,o| + o[:gemdeps] = v + end + + @installed_specs = nil end def arguments # :nodoc: @@ -39,7 +48,7 @@ class Gem::Commands::InstallCommand < Gem::Command end def defaults_str # :nodoc: - "--both --version '#{Gem::Requirement.default}' --rdoc --ri --no-force\n" \ + "--both --version '#{Gem::Requirement.default}' --document --no-force\n" \ "--install-dir #{Gem.dir}" end @@ -100,31 +109,73 @@ to write the specification by hand. For example: "#{program_name} GEMNAME [GEMNAME ...] [options] -- --build-flags" end + def install_from_gemdeps(gf) + require 'rubygems/request_set' + rs = Gem::RequestSet.new + rs.load_gemdeps gf + + rs.resolve + + specs = rs.install 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 + + raise Gem::SystemExitException, 0 + end + def execute - if options[:include_dependencies] then - alert "`gem install -y` is now default and will be removed" - alert "use --ignore-dependencies to install only the gems you list" + if gf = options[:gemdeps] then + install_from_gemdeps gf + return end - installed_gems = [] + @installed_specs = [] ENV.delete 'GEM_PATH' if options[:install_dir].nil? and RUBY_VERSION > '1.9' + if options[:install_dir] and options[:user_install] + alert_error "Use --install-dir or --user-install but not both" + terminate_interaction 1 + end + exit_code = 0 - get_all_gem_names.each do |gem_name| + if options[:version] != Gem::Requirement.default && + get_all_gem_names.size > 1 then + alert_error "Can't use --version w/ multiple gems. Use name:ver instead." + terminate_interaction 1 + end + + + get_all_gem_names_and_versions.each do |gem_name, gem_version| + gem_version ||= options[:version] + begin next if options[:conservative] and - not Gem::Dependency.new(gem_name, options[:version]).matching_specs.empty? + not Gem::Dependency.new(gem_name, gem_version).matching_specs.empty? inst = Gem::DependencyInstaller.new options - inst.install gem_name, options[:version] + inst.install gem_name, Gem::Requirement.create(gem_version) - inst.installed_gems.each do |spec| - say "Successfully installed #{spec.full_name}" - end + @installed_specs.push(*inst.installed_gems) - installed_gems.push(*inst.installed_gems) + next unless errs = inst.errors + + errs.each do |x| + next unless Gem::SourceFetchProblem === x + + msg = "Unable to pull data from '#{x.source.uri}': #{x.error.message}" + + alert_warning msg + end rescue Gem::InstallError => e alert_error "Error installing #{gem_name}:\n\t#{e.message}" exit_code |= 1 @@ -135,27 +186,9 @@ to write the specification by hand. For example: end end - unless installed_gems.empty? then - gems = installed_gems.length == 1 ? 'gem' : 'gems' - say "#{installed_gems.length} #{gems} installed" - - # NOTE: *All* of the RI documents must be generated first. For some - # reason, RI docs cannot be generated after any RDoc documents are - # generated. - - if options[:generate_ri] then - installed_gems.each do |gem| - Gem::DocManager.new(gem, options[:rdoc_args]).generate_ri - end - - Gem::DocManager.update_ri_cache - end - - if options[:generate_rdoc] then - installed_gems.each do |gem| - Gem::DocManager.new(gem, options[:rdoc_args]).generate_rdoc - end - end + unless @installed_specs.empty? then + gems = @installed_specs.length == 1 ? 'gem' : 'gems' + say "#{@installed_specs.length} #{gems} installed" end raise Gem::SystemExitException, exit_code diff --git a/lib/rubygems/commands/list_command.rb b/lib/rubygems/commands/list_command.rb index f3e5da9551..d9b7a9535e 100644 --- a/lib/rubygems/commands/list_command.rb +++ b/lib/rubygems/commands/list_command.rb @@ -7,8 +7,9 @@ require 'rubygems/commands/query_command' class Gem::Commands::ListCommand < Gem::Commands::QueryCommand - def initialize - super 'list', 'Display gems whose name starts with STRING' + def initialize(name = 'list', + summary = 'Display gems whose name starts with STRING') + super name, summary remove_option('--name-matches') end @@ -26,8 +27,9 @@ class Gem::Commands::ListCommand < Gem::Commands::QueryCommand end def execute - string = get_one_optional_argument || '' - options[:name] = /^#{string}/i + name = get_one_optional_argument || '' + options[:name] = /^#{name}/i + super end diff --git a/lib/rubygems/commands/lock_command.rb b/lib/rubygems/commands/lock_command.rb index a6dca320ef..6b4b25a281 100644 --- a/lib/rubygems/commands/lock_command.rb +++ b/lib/rubygems/commands/lock_command.rb @@ -30,7 +30,7 @@ generated. Example: - gemlock rails-1.0.0 > lockdown.rb + gem lock rails-1.0.0 > lockdown.rb will produce in lockdown.rb: diff --git a/lib/rubygems/commands/mirror_command.rb b/lib/rubygems/commands/mirror_command.rb new file mode 100644 index 0000000000..0f98077cbd --- /dev/null +++ b/lib/rubygems/commands/mirror_command.rb @@ -0,0 +1,17 @@ +require 'rubygems/command' + +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 execute + alert_error "Install the rubygems-mirror gem for the mirror command" + end + +end diff --git a/lib/rubygems/commands/outdated_command.rb b/lib/rubygems/commands/outdated_command.rb index ea6b9f0abf..887faab0a2 100644 --- a/lib/rubygems/commands/outdated_command.rb +++ b/lib/rubygems/commands/outdated_command.rb @@ -19,12 +19,15 @@ class Gem::Commands::OutdatedCommand < Gem::Command Gem::Specification.outdated.sort.each do |name| local = Gem::Specification.find_all_by_name(name).max dep = Gem::Dependency.new local.name, ">= #{local.version}" - remotes = Gem::SpecFetcher.fetcher.fetch dep + remotes, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dep next if remotes.empty? - remote = remotes.last.first - say "#{local.name} (#{local.version} < #{remote.version})" + remotes.sort! { |a,b| a[0].version <=> b[0].version } + + highest = remotes.last.first + + say "#{local.name} (#{local.version} < #{highest.version})" end end end diff --git a/lib/rubygems/commands/owner_command.rb b/lib/rubygems/commands/owner_command.rb index 6ebf9aa1aa..92674132e8 100644 --- a/lib/rubygems/commands/owner_command.rb +++ b/lib/rubygems/commands/owner_command.rb @@ -14,6 +14,10 @@ class Gem::Commands::OwnerCommand < Gem::Command "GEM gem to manage owners for" end + def usage # :nodoc: + "#{program_name} GEM" + end + def initialize super 'owner', description add_proxy_option @@ -63,12 +67,16 @@ class Gem::Commands::OwnerCommand < Gem::Command def manage_owners method, name, owners owners.each do |owner| - response = rubygems_api_request method, "api/v1/gems/#{name}/owners" do |request| - request.set_form_data 'email' => owner - request.add_field "Authorization", api_key + begin + response = rubygems_api_request method, "api/v1/gems/#{name}/owners" do |request| + request.set_form_data 'email' => owner + request.add_field "Authorization", api_key + end + + with_response response + rescue + # ignore end - - with_response response end end diff --git a/lib/rubygems/commands/pristine_command.rb b/lib/rubygems/commands/pristine_command.rb index e3771b7212..f7eb9014ea 100644 --- a/lib/rubygems/commands/pristine_command.rb +++ b/lib/rubygems/commands/pristine_command.rb @@ -1,5 +1,5 @@ require 'rubygems/command' -require 'rubygems/format' +require 'rubygems/package' require 'rubygems/installer' require 'rubygems/version_option' @@ -24,6 +24,11 @@ class Gem::Commands::PristineCommand < Gem::Command options[:extensions] = value end + add_option('--only-executables', + 'Only restore executables') do |value, options| + options[:only_executables] = value + end + add_version_option('restore to', 'pristine condition') end @@ -78,6 +83,11 @@ extensions. say "Restoring gems to pristine condition..." specs.each do |spec| + if spec.default_gem? + say "Skipped #{spec.full_name}, it is a default gem" + next + end + unless spec.extensions.empty? or options[:extensions] then say "Skipped #{spec.full_name}, it needs to compile an extension" next @@ -101,8 +111,13 @@ extensions. :wrappers => true, :force => true, :install_dir => spec.base_dir, - :env_shebang => installer_env_shebang) - installer.install + :env_shebang => installer_env_shebang, + :build_args => spec.build_args) + if options[:only_executables] then + installer.generate_bin + else + installer.install + end say "Restored #{spec.full_name}" end diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb index a7663edf4a..0667b47dc1 100644 --- a/lib/rubygems/commands/push_command.rb +++ b/lib/rubygems/commands/push_command.rb @@ -1,6 +1,7 @@ require 'rubygems/command' require 'rubygems/local_remote_options' require 'rubygems/gemcutter_utilities' +require 'rubygems/package' class Gem::Commands::PushCommand < Gem::Command include Gem::LocalRemoteOptions @@ -39,13 +40,23 @@ class Gem::Commands::PushCommand < Gem::Command def send_gem name args = [:post, "api/v1/gems"] - args << options[:host] if options[:host] if Gem.latest_rubygems_version < Gem::Version.new(Gem::VERSION) then alert_error "Using beta/unreleased version of rubygems. Not pushing." terminate_interaction 1 end + host = options[:host] + unless host + if gem_data = Gem::Package.new(name) then + host = gem_data.spec.metadata['default_gem_server'] + end + end + + args << host if host + + say "Pushing gem to #{host || Gem.host}..." + response = rubygems_api_request(*args) do |request| request.body = Gem.read_binary name request.add_field "Content-Length", request.body.size diff --git a/lib/rubygems/commands/query_command.rb b/lib/rubygems/commands/query_command.rb index 725da8787b..b6c910d449 100644 --- a/lib/rubygems/commands/query_command.rb +++ b/lib/rubygems/commands/query_command.rb @@ -21,6 +21,10 @@ class Gem::Commands::QueryCommand < Gem::Command options[:installed] = value end + add_option('-I', 'Equivalent to --no-installed') do |value, options| + options[:installed] = false + end + add_version_option command, "for use with --installed" add_option('-n', '--name-matches REGEXP', @@ -80,6 +84,7 @@ class Gem::Commands::QueryCommand < Gem::Command req = Gem::Requirement.default # TODO: deprecate for real dep = Gem::Deprecate.skip_during { Gem::Dependency.new name, req } + dep.prerelease = prerelease if local? then if prerelease and not both? then @@ -97,7 +102,7 @@ class Gem::Commands::QueryCommand < Gem::Command } spec_tuples = specs.map do |spec| - [[spec.name, spec.version, spec.original_platform, spec], :local] + [spec.name_tuple, spec] end output_query_results spec_tuples @@ -110,13 +115,27 @@ class Gem::Commands::QueryCommand < Gem::Command say end - all = options[:all] - fetcher = Gem::SpecFetcher.fetcher - spec_tuples = fetcher.find_matching dep, all, false, prerelease - spec_tuples += fetcher.find_matching dep, false, false, true if - prerelease and all + type = if options[:all] + if options[:prerelease] + :complete + else + :released + end + elsif options[:prerelease] + :prerelease + else + :latest + end + + if options[:name].source.empty? + spec_tuples = fetcher.detect(type) { true } + else + spec_tuples = fetcher.detect(type) do |gem_name, ver, plat| + options[:name] === gem_name + end + end output_query_results spec_tuples end @@ -135,32 +154,30 @@ class Gem::Commands::QueryCommand < Gem::Command output = [] versions = Hash.new { |h,name| h[name] = [] } - spec_tuples.each do |spec_tuple, source_uri| - versions[spec_tuple.first] << [spec_tuple, source_uri] + spec_tuples.each do |spec_tuple, source| + versions[spec_tuple.name] << [spec_tuple, source] end - versions = versions.sort_by do |(name,_),_| - name.downcase + versions = versions.sort_by do |(n,_),_| + n.downcase end versions.each do |gem_name, matching_tuples| - matching_tuples = matching_tuples.sort_by do |(_, version,_),_| - version - end.reverse + matching_tuples = matching_tuples.sort_by { |n,_| n.version }.reverse platforms = Hash.new { |h,version| h[version] = [] } - matching_tuples.map do |(_, version, platform,_),_| - platforms[version] << platform if platform + matching_tuples.map do |n,_| + platforms[n.version] << n.platform if n.platform end seen = {} - matching_tuples.delete_if do |(_, version,_),_| - if seen[version] then + matching_tuples.delete_if do |n,_| + if seen[n.version] then true else - seen[version] = true + seen[n.version] = true false end end @@ -169,7 +186,7 @@ class Gem::Commands::QueryCommand < Gem::Command if options[:versions] then list = if platforms.empty? or options[:details] then - matching_tuples.map { |(_, version,_),_| version }.uniq + matching_tuples.map { |n,_| n.version }.uniq else platforms.sort.reverse.map do |version, pls| if pls == [Gem::Platform::RUBY] then @@ -188,12 +205,11 @@ class Gem::Commands::QueryCommand < Gem::Command if options[:details] then detail_tuple = matching_tuples.first - spec = if detail_tuple.first.length == 4 then - detail_tuple.first.last - else - uri = URI.parse detail_tuple.last - Gem::SpecFetcher.fetcher.fetch_spec detail_tuple.first, uri - end + spec = detail_tuple.last + + unless spec.kind_of? Gem::Specification + spec = spec.fetch_spec detail_tuple.first + end entry << "\n" @@ -243,9 +259,9 @@ class Gem::Commands::QueryCommand < Gem::Command entry << "\n" << " Installed at: #{loaded_from}" else label = 'Installed at' - matching_tuples.each do |(_,version,_,s),| + matching_tuples.each do |n,s| loaded_from = File.dirname File.dirname(s.loaded_from) - entry << "\n" << " #{label} (#{version}): #{loaded_from}" + entry << "\n" << " #{label} (#{n.version}): #{loaded_from}" label = ' ' * label.length end end diff --git a/lib/rubygems/commands/rdoc_command.rb b/lib/rubygems/commands/rdoc_command.rb index ea0f3ad592..9bb07245cd 100644 --- a/lib/rubygems/commands/rdoc_command.rb +++ b/lib/rubygems/commands/rdoc_command.rb @@ -1,6 +1,6 @@ require 'rubygems/command' require 'rubygems/version_option' -require 'rubygems/doc_manager' +require 'rubygems/rdoc' class Gem::Commands::RdocCommand < Gem::Command include Gem::VersionOption @@ -8,7 +8,7 @@ class Gem::Commands::RdocCommand < Gem::Command def initialize super 'rdoc', 'Generates RDoc for pre-installed gems', :version => Gem::Requirement.default, - :include_rdoc => true, :include_ri => true, :overwrite => false + :include_rdoc => false, :include_ri => true, :overwrite => false add_option('--all', 'Generate RDoc/RI documentation for all', @@ -39,7 +39,7 @@ class Gem::Commands::RdocCommand < Gem::Command end def defaults_str # :nodoc: - "--version '#{Gem::Requirement.default}' --rdoc --ri --no-overwrite" + "--version '#{Gem::Requirement.default}' --ri --no-overwrite" end def description # :nodoc: @@ -54,37 +54,32 @@ The rdoc command builds RDoc and RI documentation for installed gems. Use end def execute - if options[:all] then - specs = Gem::SourceIndex.from_installed_gems.collect { |name, spec| - spec - } - else - gem_name = get_one_gem_name - dep = Gem::Dependency.new gem_name, options[:version] - specs = Gem::SourceIndex.from_installed_gems.search dep + specs = if options[:all] then + Gem::Specification.to_a + else + get_all_gem_names.map do |name| + Gem::Specification.find_by_name name, options[:version] + end.flatten.uniq + end + + if specs.empty? then + alert_error 'No matching gems found' + terminate_interaction 1 end - if specs.empty? - raise "Failed to find gem #{gem_name} to generate RDoc for #{options[:version]}" - end + specs.each do |spec| + doc = Gem::RDoc.new spec, options[:include_rdoc], options[:include_ri] - if options[:include_ri] - specs.sort.each do |spec| - doc = Gem::DocManager.new(spec) - doc.generate_ri if options[:overwrite] || !doc.ri_installed? - end + doc.force = options[:overwrite] - Gem::DocManager.update_ri_cache - end - - if options[:include_rdoc] - specs.sort.each do |spec| - doc = Gem::DocManager.new(spec) - doc.generate_rdoc if options[:overwrite] || !doc.rdoc_installed? + begin + doc.generate + rescue Errno::ENOENT => e + e.message =~ / - / + alert_error "Unable to document #{spec.full_name}, #{$'} is missing, skipping" + terminate_interaction 1 if specs.length == 1 end end - - true end end diff --git a/lib/rubygems/commands/search_command.rb b/lib/rubygems/commands/search_command.rb index 52e96fd1ef..92d4b3672e 100644 --- a/lib/rubygems/commands/search_command.rb +++ b/lib/rubygems/commands/search_command.rb @@ -1,30 +1,16 @@ require 'rubygems/command' -require 'rubygems/commands/query_command' +require 'rubygems/commands/list_command' -class Gem::Commands::SearchCommand < Gem::Commands::QueryCommand +class Gem::Commands::SearchCommand < Gem::Commands::ListCommand def initialize super 'search', 'Display all gems whose name contains STRING' - remove_option '--name-matches' - end - - def arguments # :nodoc: - "STRING fragment of gem name to search for" + @defaults[:domain] = :remote end def defaults_str # :nodoc: - "--local --no-details" - end - - def usage # :nodoc: - "#{program_name} [STRING]" - end - - def execute - string = get_one_optional_argument - options[:name] = /#{string}/i - super + "--remote --no-details" end end diff --git a/lib/rubygems/commands/server_command.rb b/lib/rubygems/commands/server_command.rb index b65d48c4fc..4796ce2ad6 100644 --- a/lib/rubygems/commands/server_command.rb +++ b/lib/rubygems/commands/server_command.rb @@ -78,7 +78,7 @@ You can set up a shortcut to gem server documentation using the URL: end def execute - options[:gemdir] << Gem.dir if options[:gemdir].empty? + options[:gemdir] = Gem.path if options[:gemdir].empty? Gem::Server.run options end diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb index 508ff84a0f..2f1cf0091d 100644 --- a/lib/rubygems/commands/setup_command.rb +++ b/lib/rubygems/commands/setup_command.rb @@ -5,14 +5,22 @@ require 'rubygems/command' # RubyGems checkout or tarball. class Gem::Commands::SetupCommand < Gem::Command + HISTORY_HEADER = /^===\s*[\d.]+\s*\/\s*\d{4}-\d{2}-\d{2}\s*$/ + VERSION_MATCHER = /^===\s*([\d.]+)\s*\/\s*\d{4}-\d{2}-\d{2}\s*$/ def initialize require 'tmpdir' super 'setup', 'Install RubyGems', - :format_executable => true, :rdoc => true, :ri => true, + :format_executable => true, :document => %w[ri], :site_or_vendor => :sitelibdir, - :destdir => '', :prefix => '' + :destdir => '', :prefix => '', :previous_version => '' + + 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', @@ -37,14 +45,37 @@ class Gem::Commands::SetupCommand < Gem::Command 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| - options[:rdoc] = value + if value then + 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| - options[:ri] = value + if value then + options[:document] << 'ri' + else + options[:document].delete 'ri' + end + + options[:document].uniq! end end @@ -58,7 +89,7 @@ class Gem::Commands::SetupCommand < Gem::Command end def defaults_str # :nodoc: - "--format-executable --rdoc --ri" + "--format-executable --document ri" end def description # :nodoc: @@ -110,7 +141,7 @@ By default, this RubyGems will install gem as: uninstall_old_gemcutter - install_rdoc + documentation_success = install_rdoc say if @verbose then @@ -118,14 +149,30 @@ By default, this RubyGems will install gem as: 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]) + release_notes = File.join Dir.pwd, 'History.txt' release_notes = if File.exist? release_notes then - open release_notes do |io| - text = io.gets '===' - text << io.gets('===') - text[0...-3].sub(/^# coding:.*?^=/m, '') + history = File.read release_notes + history = history.sub(/^# coding:.*?^=/m, '') + + 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 { |x| Gem::Version.new(x) } + + history_string = "" + + until versions.length == 0 or versions.shift < options[:previous_version] + history_string += version_lines.shift + text.shift end + + history_string else "Oh-no! Unable to find release notes!" end @@ -145,6 +192,31 @@ By default, this RubyGems will install gem as: say "to remove it by hand." say end + + if documentation_success + if options[:document].include? 'rdoc' then + 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' then + 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) @@ -165,7 +237,7 @@ By default, this RubyGems will install gem as: end dest_file = File.join bin_dir, bin_file_formatted - bin_tmp_file = File.join Dir.tmpdir, bin_file + bin_tmp_file = File.join Dir.tmpdir, "#{bin_file}.#{$$}" begin bin = File.readlines bin_file @@ -209,10 +281,7 @@ TEXT say "Installing RubyGems" if @verbose Dir.chdir 'lib' do - lib_files = Dir[File.join('**', '*rb')] - - # Be sure to include our SSL ca bundles - lib_files += Dir[File.join('**', '*pem')] + lib_files = Dir[File.join('**', '*rb')] lib_files.each do |lib_file| dest_file = File.join lib_dir, lib_file @@ -229,6 +298,12 @@ TEXT 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 and (not File.exist? rubygems_doc_dir or File.writable? rubygems_doc_dir) then @@ -237,21 +312,26 @@ TEXT rm_rf dir end - if options[:ri] then - ri_dir = File.join rubygems_doc_dir, 'ri' - say "Installing #{rubygems_name} ri into #{ri_dir}" if @verbose - run_rdoc '--ri', '--op', ri_dir - end + require 'rubygems/rdoc' - if options[:rdoc] then - rdoc_dir = File.join rubygems_doc_dir, 'rdoc' - say "Installing #{rubygems_name} rdoc into #{rdoc_dir}" if @verbose - run_rdoc '--op', rdoc_dir + fake_spec = Gem::Specification.new 'rubygems', Gem::VERSION + def fake_spec.full_gem_path + File.expand_path '../../../..', __FILE__ 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 then say "Skipping RDoc generation, #{gem_doc_dir} not writable" say "Set the GEM_HOME environment variable if you want RDoc generated" end + + return false end def make_destination_dirs(install_destdir) @@ -331,23 +411,6 @@ abort "#{deprecation_message}" end end - def run_rdoc(*args) - begin - gem 'rdoc' - rescue Gem::LoadError - end - - require 'rdoc/rdoc' - - args << '--main' << 'README.rdoc' << '--quiet' - args << '.' - args << 'README.rdoc' << 'UPGRADING.rdoc' - args << 'LICENSE.txt' << 'MIT.txt' << 'History.txt' - - r = RDoc::RDoc.new - r.document args - end - def uninstall_old_gemcutter require 'rubygems/uninstaller' diff --git a/lib/rubygems/commands/sources_command.rb b/lib/rubygems/commands/sources_command.rb index ac14313e9d..97ed7329ea 100644 --- a/lib/rubygems/commands/sources_command.rb +++ b/lib/rubygems/commands/sources_command.rb @@ -48,7 +48,7 @@ class Gem::Commands::SourcesCommand < Gem::Command options[:update]) if options[:clear_all] then - path = Gem::SpecFetcher.fetcher.dir + path = File.join Gem.user_home, '.gem', 'specs' FileUtils.rm_rf path unless File.exist? path then @@ -64,16 +64,19 @@ class Gem::Commands::SourcesCommand < Gem::Command end end - if options[:add] then - source_uri = options[:add] - uri = URI.parse source_uri + if source_uri = options[:add] then + source = Gem::Source.new source_uri begin - Gem::SpecFetcher.fetcher.load_specs uri, 'specs' - Gem.sources << source_uri - Gem.configuration.write + if Gem.sources.include? source_uri then + 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" + say "#{source_uri} added to sources" + end rescue URI::Error, ArgumentError say "#{source_uri} is not a URI" terminate_interaction 1 @@ -97,12 +100,9 @@ class Gem::Commands::SourcesCommand < Gem::Command end if options[:update] then - fetcher = Gem::SpecFetcher.fetcher - - Gem.sources.each do |update_uri| - update_uri = URI.parse update_uri - fetcher.load_specs update_uri, 'specs' - fetcher.load_specs update_uri, 'latest_specs' + Gem.sources.each_source do |src| + src.load_specs :released + src.load_specs :latest end say "source cache successfully updated" @@ -112,8 +112,8 @@ class Gem::Commands::SourcesCommand < Gem::Command say "*** CURRENT SOURCES ***" say - Gem.sources.each do |source| - say source + Gem.sources.each do |src| + say src end end end diff --git a/lib/rubygems/commands/specification_command.rb b/lib/rubygems/commands/specification_command.rb index 566a9cc66e..63da5fef0f 100644 --- a/lib/rubygems/commands/specification_command.rb +++ b/lib/rubygems/commands/specification_command.rb @@ -1,7 +1,7 @@ require 'rubygems/command' require 'rubygems/local_remote_options' require 'rubygems/version_option' -require 'rubygems/format' +require 'rubygems/package' class Gem::Commands::SpecificationCommand < Gem::Command @@ -17,6 +17,7 @@ class Gem::Commands::SpecificationCommand < Gem::Command add_version_option('examine') add_platform_option + add_prerelease_option add_option('--all', 'Output specifications for all versions of', 'the gem') do |value, options| @@ -62,13 +63,13 @@ FIELD name of gemspec field to show "Please specify a gem name or file on the command line" end - case options[:version] + case v = options[:version] when String - req = Gem::Requirement.parse options[:version] + req = Gem::Requirement.create v when Gem::Requirement - req = options[:version] + req = v else - raise Gem::CommandLineError, "Unsupported version type: #{options[:version]}" + raise Gem::CommandLineError, "Unsupported version type: '#{v}'" end if !req.none? and options[:all] @@ -79,7 +80,7 @@ FIELD name of gemspec field to show if options[:all] dep = Gem::Dependency.new gem else - dep = Gem::Dependency.new gem, options[:version] + dep = Gem::Dependency.new gem, req end field = get_one_optional_argument @@ -89,7 +90,7 @@ FIELD name of gemspec field to show if local? then if File.exist? gem then - specs << Gem::Format.from_file_by_path(gem).spec rescue nil + specs << Gem::Package.new(gem).spec rescue nil end if specs.empty? then @@ -98,17 +99,14 @@ FIELD name of gemspec field to show end if remote? then - found = Gem::SpecFetcher.fetcher.fetch dep, true - - if dep.prerelease? or options[:prerelease] - found += Gem::SpecFetcher.fetcher.fetch dep, false, true, true - end + dep.prerelease = options[:prerelease] + found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dep specs.push(*found.map { |spec,| spec }) end if specs.empty? then - alert_error "Unknown gem '#{gem}'" + alert_error "No gem matching '#{dep}' found" terminate_interaction 1 end diff --git a/lib/rubygems/commands/uninstall_command.rb b/lib/rubygems/commands/uninstall_command.rb index aaadb762b5..736574ddff 100644 --- a/lib/rubygems/commands/uninstall_command.rb +++ b/lib/rubygems/commands/uninstall_command.rb @@ -13,7 +13,8 @@ class Gem::Commands::UninstallCommand < Gem::Command def initialize super 'uninstall', 'Uninstall gems from the local repository', - :version => Gem::Requirement.default, :user_install => true + :version => Gem::Requirement.default, :user_install => true, + :check_dev => false add_option('-a', '--[no-]all', 'Uninstall all matching versions' @@ -27,6 +28,12 @@ class Gem::Commands::UninstallCommand < Gem::Command 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| @@ -54,6 +61,12 @@ class Gem::Commands::UninstallCommand < Gem::Command 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_version_option add_platform_option end @@ -73,19 +86,23 @@ class Gem::Commands::UninstallCommand < Gem::Command end def execute - original_path = Gem.path + # REFACTOR: stolen from cleanup_command + deplist = Gem::DependencyList.new + get_all_gem_names.uniq.each do |name| + Gem::Specification.find_all_by_name(name).each do |spec| + deplist.add spec + end + end + + deps = deplist.strongly_connected_components.flatten.reverse - get_all_gem_names.each do |gem_name| + deps.map(&:name).uniq.each do |gem_name| begin Gem::Uninstaller.new(gem_name, options).uninstall - rescue Gem::InstallError => e - alert e.message rescue Gem::GemNotInHomeException => e spec = e.spec alert("In order to remove #{spec.name}, please execute:\n" \ "\tgem uninstall #{spec.name} --install-dir=#{spec.installation_path}") - ensure - Gem.use_paths(*original_path) end end end diff --git a/lib/rubygems/commands/unpack_command.rb b/lib/rubygems/commands/unpack_command.rb index 64b8ad64f8..7eefd32a6e 100644 --- a/lib/rubygems/commands/unpack_command.rb +++ b/lib/rubygems/commands/unpack_command.rb @@ -69,8 +69,10 @@ class Gem::Commands::UnpackCommand < Gem::Command else basename = File.basename path, '.gem' target_dir = File.expand_path basename, options[:target] - FileUtils.mkdir_p target_dir - Gem::Installer.new(path, :unpack => true).unpack target_dir + + package = Gem::Package.new path + package.extract_files target_dir + say "Unpacked gem: '#{target_dir}'" end end @@ -134,9 +136,11 @@ class Gem::Commands::UnpackCommand < Gem::Command ## # Extracts the Gem::Specification and raw metadata from the .gem file at # +path+. + #-- + # TODO move to Gem::Package as #raw_spec or something def get_metadata path - format = Gem::Format.from_file_by_path path + format = Gem::Package.new path spec = format.spec metadata = nil diff --git a/lib/rubygems/commands/update_command.rb b/lib/rubygems/commands/update_command.rb index d63b943c56..02f9657435 100644 --- a/lib/rubygems/commands/update_command.rb +++ b/lib/rubygems/commands/update_command.rb @@ -1,10 +1,12 @@ require 'rubygems/command' require 'rubygems/command_manager' +require 'rubygems/dependency_installer' require 'rubygems/install_update_options' require 'rubygems/local_remote_options' require 'rubygems/spec_fetcher' require 'rubygems/version_option' -require 'rubygems/commands/install_command' +require 'rubygems/install_message' # must come before rdoc for messaging +require 'rubygems/rdoc' class Gem::Commands::UpdateCommand < Gem::Command @@ -13,11 +15,9 @@ class Gem::Commands::UpdateCommand < Gem::Command include Gem::VersionOption def initialize - super 'update', - 'Update the named gems (or all installed gems) in the local repository', - :generate_rdoc => true, - :generate_ri => true, - :force => false + super 'update', 'Update installed gems to the latest version', + :document => %w[rdoc ri], + :force => false add_install_update_options @@ -37,6 +37,9 @@ class Gem::Commands::UpdateCommand < Gem::Command add_local_remote_options add_platform_option add_prerelease_option "as update targets" + + @updated = [] + @installer = Gem::DependencyInstaller.new options end def arguments # :nodoc: @@ -44,7 +47,7 @@ class Gem::Commands::UpdateCommand < Gem::Command end def defaults_str # :nodoc: - "--rdoc --ri --no-force --install-dir #{Gem.dir}" + "--document --no-force --install-dir #{Gem.dir}" end def usage # :nodoc: @@ -52,9 +55,6 @@ class Gem::Commands::UpdateCommand < Gem::Command end def execute - @installer = Gem::DependencyInstaller.new options - @updated = [] - hig = {} if options[:system] then @@ -79,21 +79,7 @@ class Gem::Commands::UpdateCommand < Gem::Command if updated.empty? then say "Nothing to update" else - say "Gems updated: #{updated.map { |spec| spec.name }.join ', '}" - - if options[:generate_ri] then - updated.each do |gem| - Gem::DocManager.new(gem, options[:rdoc_args]).generate_ri - end - - Gem::DocManager.update_ri_cache - end - - if options[:generate_rdoc] then - updated.each do |gem| - Gem::DocManager.new(gem, options[:rdoc_args]).generate_rdoc - end - end + say "Gems updated: #{updated.map { |spec| spec.name }.join ' '}" end end @@ -112,7 +98,6 @@ class Gem::Commands::UpdateCommand < Gem::Command @installer.installed_gems.each do |spec| @updated << spec - say "Successfully installed #{spec.full_name}" if success end end @@ -178,8 +163,9 @@ class Gem::Commands::UpdateCommand < Gem::Command args = [] args << '--prefix' << Gem.prefix if Gem.prefix - args << '--no-rdoc' unless options[:generate_rdoc] - args << '--no-ri' unless options[:generate_ri] + # TODO use --document for >= 1.9 , --no-rdoc --no-ri < 1.9 + args << '--no-rdoc' unless options[:document].include? 'rdoc' + args << '--no-ri' unless options[:document].include? 'ri' args << '--no-format-executable' if options[:no_format_executable] update_dir = File.join Gem.dir, 'gems', "rubygems-update-#{version}" @@ -205,20 +191,20 @@ class Gem::Commands::UpdateCommand < Gem::Command gem_names.all? { |name| /#{name}/ !~ l_spec.name } dependency = Gem::Dependency.new l_spec.name, "> #{l_spec.version}" + dependency.prerelease = options[:prerelease] fetcher = Gem::SpecFetcher.fetcher - spec_tuples = fetcher.find_matching dependency - matching_gems = spec_tuples.select do |(name, _, platform),| - name == l_name and Gem::Platform.match platform + spec_tuples, _ = fetcher.search_for_dependency dependency + + matching_gems = spec_tuples.select do |g,_| + g.name == l_name and g.match_platform? end - highest_remote_gem = matching_gems.sort_by do |(_, version),| - version - end.last + highest_remote_gem = matching_gems.sort_by { |g,_| g.version }.last - highest_remote_gem ||= [[nil, Gem::Version.new(0), nil]] # "null" object - highest_remote_ver = highest_remote_gem.first[1] + highest_remote_gem ||= [Gem::NameTuple.null] + highest_remote_ver = highest_remote_gem.first.version if system or (l_spec.version < highest_remote_ver) then result << [l_spec.name, [l_spec.version, highest_remote_ver].max] diff --git a/lib/rubygems/commands/yank_command.rb b/lib/rubygems/commands/yank_command.rb new file mode 100644 index 0000000000..da0cf7ad3b --- /dev/null +++ b/lib/rubygems/commands/yank_command.rb @@ -0,0 +1,98 @@ +require 'rubygems/command' +require 'rubygems/local_remote_options' +require 'rubygems/version_option' +require 'rubygems/gemcutter_utilities' + +class Gem::Commands::YankCommand < Gem::Command + include Gem::LocalRemoteOptions + include Gem::VersionOption + include Gem::GemcutterUtilities + + def description # :nodoc: + 'Remove a specific gem version release from RubyGems.org' + end + + def arguments # :nodoc: + "GEM name of gem" + end + + def usage # :nodoc: + "#{program_name} GEM -v VERSION [-p PLATFORM] [--undo] [--key KEY_NAME]" + end + + def initialize + super 'yank', description + + add_version_option("remove") + add_platform_option("remove") + + add_option('--undo') do |value, options| + options[:undo] = true + end + + add_option('-k', '--key KEY_NAME', + 'Use API key from your gem credentials file') do |value, options| + options[:key] = value + end + end + + def execute + sign_in + + version = get_version_from_requirements(options[:version]) + platform = get_platform_from_requirements(options) + api_key = Gem.configuration.rubygems_api_key + api_key = Gem.configuration.api_keys[options[:key].to_sym] if options[:key] + + if version then + if options[:undo] then + unyank_gem(version, platform, api_key) + else + yank_gem(version, platform, api_key) + end + else + say "A version argument is required: #{usage}" + terminate_interaction + end + end + + def yank_gem(version, platform, api_key) + say "Yanking gem from #{self.host}..." + yank_api_request(:delete, version, platform, "api/v1/gems/yank", api_key) + end + + def unyank_gem(version, platform, api_key) + say "Unyanking gem from #{host}..." + yank_api_request(:put, version, platform, "api/v1/gems/unyank", api_key) + end + + private + + def yank_api_request(method, version, platform, api, api_key) + name = get_one_gem_name + response = rubygems_api_request(method, api) 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 + say response.body + end + + def get_version_from_requirements(requirements) + requirements.requirements.first[1].version + rescue + nil + end + + def get_platform_from_requirements(requirements) + Gem.platforms[1].to_s if requirements.key? :added_platform + end + +end + diff --git a/lib/rubygems/compatibility.rb b/lib/rubygems/compatibility.rb new file mode 100644 index 0000000000..1137407bc5 --- /dev/null +++ b/lib/rubygems/compatibility.rb @@ -0,0 +1,51 @@ +# This file contains all sorts of little compatibility hacks that we've +# had to introduce over the years. Quarantining them into one file helps +# us know when we can get rid of them. + +# Ruby 1.9.x has introduced some things that are awkward, and we need to +# support them, so we define some constants to use later. +module Gem + # Only MRI 1.9.2 has the custom prelude. + GEM_PRELUDE_SUCKAGE = RUBY_VERSION =~ /^1\.9\.2/ and RUBY_ENGINE == "ruby" +end + +# Gem::QuickLoader exists in the gem prelude code in ruby 1.9.2 itself. +# We gotta get rid of it if it's there, before we do anything else. +if Gem::GEM_PRELUDE_SUCKAGE and defined?(Gem::QuickLoader) then + Gem::QuickLoader.remove + + $LOADED_FEATURES.delete Gem::QuickLoader.path_to_full_rubygems_library + + if $LOADED_FEATURES.any? do |path| path.end_with? '/rubygems.rb' end then + # TODO path does not exist here + raise LoadError, "another rubygems is already loaded from #{path}" + end + + class << Gem + remove_method :try_activate if Gem.respond_to?(:try_activate, true) + end +end + +module Gem + RubyGemsVersion = VERSION + + RbConfigPriorities = %w[ + EXEEXT RUBY_SO_NAME arch bindir datadir libdir ruby_install_name + ruby_version rubylibprefix sitedir sitelibdir vendordir vendorlibdir + rubylibdir + ] + + unless defined?(ConfigMap) + ## + # Configuration settings from ::RbConfig + ConfigMap = Hash.new do |cm, key| + cm[key] = RbConfig::CONFIG[key.to_s] + end + else + RbConfigPriorities.each do |key| + ConfigMap[key.to_sym] = RbConfig::CONFIG[key] + end + end + + RubyGemsPackageVersion = VERSION +end diff --git a/lib/rubygems/config_file.rb b/lib/rubygems/config_file.rb index 136e8b4610..61ee3b73eb 100644 --- a/lib/rubygems/config_file.rb +++ b/lib/rubygems/config_file.rb @@ -5,9 +5,9 @@ #++ ## -# Gem::ConfigFile RubyGems options and gem command options from ~/.gemrc. +# Gem::ConfigFile RubyGems options and gem command options from gemrc. # -# ~/.gemrc is a YAML file that uses strings to match gem command arguments and +# gemrc is a YAML file that uses strings to match gem command arguments and # symbols to match RubyGems options. # # Gem command arguments use a String key that matches the command name and @@ -21,16 +21,19 @@ # RubyGems options use symbol keys. Valid options are: # # +:backtrace+:: See #backtrace -# +:benchmark+:: See #benchmark # +:sources+:: Sets Gem::sources # +:verbose+:: See #verbose - -require 'rbconfig' +# +# gemrc files may exist in various locations and are read and merged in +# the following order: +# +# - system wide (/etc/gemrc) +# - per user (~/.gemrc) +# - per environment (gemrc files listed in the GEMRC environment variable) class Gem::ConfigFile DEFAULT_BACKTRACE = false - DEFAULT_BENCHMARK = false DEFAULT_BULK_THRESHOLD = 1000 DEFAULT_VERBOSITY = true DEFAULT_UPDATE_SOURCES = true @@ -96,11 +99,6 @@ class Gem::ConfigFile attr_writer :backtrace - ## - # True if we are benchmarking this run. - - attr_accessor :benchmark - ## # Bulk threshold value. If the number of missing gems are above this # threshold value, then a bulk download technique is used. (deprecated) @@ -131,6 +129,10 @@ class Gem::ConfigFile attr_reader :api_keys ## + # True if we want to force specification of gem server when pushing a gem + + attr_accessor :disable_default_gem_server + # openssl verify mode value, used for remote https connection attr_reader :ssl_verify_mode @@ -158,29 +160,29 @@ class Gem::ConfigFile # --debug:: # Enable Ruby level debug messages. Handled early for the same reason as # --backtrace. + #-- + # TODO: parse options upstream, pass in options directly - def initialize(arg_list) + def initialize(args) @config_file_name = nil need_config_file_name = false - arg_list = arg_list.map do |arg| + arg_list = [] + + args.each do |arg| if need_config_file_name then @config_file_name = arg need_config_file_name = false - nil elsif arg =~ /^--config-file=(.*)/ then @config_file_name = $1 - nil elsif arg =~ /^--config-file$/ then need_config_file_name = true - nil else - arg + arg_list << arg end - end.compact + end @backtrace = DEFAULT_BACKTRACE - @benchmark = DEFAULT_BENCHMARK @bulk_threshold = DEFAULT_BULK_THRESHOLD @verbose = DEFAULT_VERBOSITY @update_sources = DEFAULT_UPDATE_SOURCES @@ -189,19 +191,25 @@ class Gem::ConfigFile platform_config = Marshal.load Marshal.dump(PLATFORM_DEFAULTS) system_config = load_file SYSTEM_WIDE_CONFIG_FILE user_config = load_file config_file_name.dup.untaint + environment_config = (ENV['GEMRC'] || '').split(/[:;]/).inject({}) do |result, file| + result.merge load_file file + end + @hash = operating_system_config.merge platform_config @hash = @hash.merge system_config @hash = @hash.merge user_config + @hash = @hash.merge environment_config # HACK these override command-line args, which is bad - @backtrace = @hash[:backtrace] if @hash.key? :backtrace - @benchmark = @hash[:benchmark] if @hash.key? :benchmark - @bulk_threshold = @hash[:bulk_threshold] if @hash.key? :bulk_threshold - @home = @hash[:gemhome] if @hash.key? :gemhome - @path = @hash[:gempath] if @hash.key? :gempath - @update_sources = @hash[:update_sources] if @hash.key? :update_sources - @verbose = @hash[:verbose] if @hash.key? :verbose + @backtrace = @hash[:backtrace] if @hash.key? :backtrace + @bulk_threshold = @hash[:bulk_threshold] if @hash.key? :bulk_threshold + @home = @hash[:gemhome] if @hash.key? :gemhome + @path = @hash[:gempath] if @hash.key? :gempath + @update_sources = @hash[:update_sources] if @hash.key? :update_sources + @verbose = @hash[:verbose] if @hash.key? :verbose + @disable_default_gem_server = @hash[:disable_default_gem_server] if @hash.key? :disable_default_gem_server + @ssl_verify_mode = @hash[:ssl_verify_mode] if @hash.key? :ssl_verify_mode @ssl_ca_cert = @hash[:ssl_ca_cert] if @hash.key? :ssl_ca_cert @@ -224,6 +232,7 @@ class Gem::ConfigFile else @hash end + if @api_keys.key? :rubygems_api_key then @rubygems_api_key = @api_keys[:rubygems_api_key] @api_keys[:rubygems] = @api_keys.delete :rubygems_api_key unless @api_keys.key? :rubygems @@ -238,7 +247,8 @@ class Gem::ConfigFile Gem.load_yaml - File.open(credentials_path, 'w') do |f| + permissions = 0600 & (~File.umask) + File.open(credentials_path, 'w', permissions) do |f| f.write config.to_yaml end @@ -249,13 +259,21 @@ class Gem::ConfigFile Gem.load_yaml return {} unless filename and File.exist? filename + begin - YAML.load(File.read(filename)) + content = YAML.load(File.read(filename)) + unless content.kind_of? Hash + warn "Failed to load #{config_file_name} because it doesn't contain valid YAML hash" + return {} + end + return content rescue ArgumentError warn "Failed to load #{config_file_name}" rescue Errno::EACCES warn "Failed to load #{config_file_name} due to permissions problem." - end or {} + end + + {} end # True if the backtrace option has been specified, or debug is on. @@ -273,13 +291,11 @@ class Gem::ConfigFile hash = @hash.dup hash.delete :update_sources hash.delete :verbose - hash.delete :benchmark hash.delete :backtrace hash.delete :bulk_threshold yield :update_sources, @update_sources yield :verbose, @verbose - yield :benchmark, @benchmark yield :backtrace, @backtrace yield :bulk_threshold, @bulk_threshold @@ -296,8 +312,6 @@ class Gem::ConfigFile case arg when /^--(backtrace|traceback)$/ then @backtrace = true - when /^--bench(mark)?$/ then - @benchmark = true when /^--debug$/ then $DEBUG = true else @@ -309,25 +323,41 @@ class Gem::ConfigFile # Really verbose mode gives you extra output. def really_verbose case verbose - when true, false, nil then false - else true + when true, false, nil then + false + else + true end end # to_yaml only overwrites things you can't override on the command line. def to_yaml # :nodoc: yaml_hash = {} - yaml_hash[:backtrace] = @hash.key?(:backtrace) ? @hash[:backtrace] : - DEFAULT_BACKTRACE - yaml_hash[:benchmark] = @hash.key?(:benchmark) ? @hash[:benchmark] : - DEFAULT_BENCHMARK - yaml_hash[:bulk_threshold] = @hash.key?(:bulk_threshold) ? - @hash[:bulk_threshold] : DEFAULT_BULK_THRESHOLD - yaml_hash[:sources] = Gem.sources - yaml_hash[:update_sources] = @hash.key?(:update_sources) ? - @hash[:update_sources] : DEFAULT_UPDATE_SOURCES - yaml_hash[:verbose] = @hash.key?(:verbose) ? @hash[:verbose] : - DEFAULT_VERBOSITY + yaml_hash[:backtrace] = if @hash.key?(:backtrace) + @hash[:backtrace] + else + DEFAULT_BACKTRACE + end + + yaml_hash[:bulk_threshold] = if @hash.key?(:bulk_threshold) + @hash[:bulk_threshold] + else + DEFAULT_BULK_THRESHOLD + end + + yaml_hash[:sources] = Gem.sources.to_a + + yaml_hash[:update_sources] = if @hash.key?(:update_sources) + @hash[:update_sources] + else + DEFAULT_UPDATE_SOURCES + end + + yaml_hash[:verbose] = if @hash.key?(:verbose) + @hash[:verbose] + else + DEFAULT_VERBOSITY + end keys = yaml_hash.keys.map { |key| key.to_s } keys << 'debug' @@ -361,15 +391,13 @@ class Gem::ConfigFile def ==(other) # :nodoc: self.class === other and - @backtrace == other.backtrace and - @benchmark == other.benchmark and - @bulk_threshold == other.bulk_threshold and - @verbose == other.verbose and - @update_sources == other.update_sources and - @hash == other.hash + @backtrace == other.backtrace and + @bulk_threshold == other.bulk_threshold and + @verbose == other.verbose and + @update_sources == other.update_sources and + @hash == other.hash end - protected - attr_reader :hash + protected :hash end diff --git a/lib/rubygems/core_ext/kernel_gem.rb b/lib/rubygems/core_ext/kernel_gem.rb new file mode 100644 index 0000000000..f946d0d5d7 --- /dev/null +++ b/lib/rubygems/core_ext/kernel_gem.rb @@ -0,0 +1,53 @@ +module Kernel + + # REFACTOR: This should be pulled out into some kind of hacks file. + remove_method :gem if 'method' == defined? gem # from gem_prelude.rb on 1.9 + + ## + # Use Kernel#gem to activate a specific version of +gem_name+. + # + # +requirements+ is a list of version requirements that the + # specified gem must match, most commonly "= example.version.number". See + # Gem::Requirement for how to specify a version requirement. + # + # If you will be activating the latest version of a gem, there is no need to + # call Kernel#gem, Kernel#require will do the right thing for you. + # + # Kernel#gem returns true if the gem was activated, otherwise false. If the + # gem could not be found, didn't match the version requirements, or a + # different version was already activated, an exception will be raised. + # + # Kernel#gem should be called *before* any require statements (otherwise + # RubyGems may load a conflicting library version). + # + # In older RubyGems versions, the environment variable GEM_SKIP could be + # used to skip activation of specified gems, for example to test out changes + # that haven't been installed yet. Now RubyGems defers to -I and the + # RUBYLIB environment variable to skip activation of a gem. + # + # Example: + # + # GEM_SKIP=libA:libB ruby -I../libA -I../libB ./mycode.rb + + def gem(gem_name, *requirements) # :doc: + skip_list = (ENV['GEM_SKIP'] || "").split(/:/) + raise Gem::LoadError, "skipping #{gem_name}" if skip_list.include? gem_name + + if gem_name.kind_of? Gem::Dependency + unless Gem::Deprecate.skip + warn "#{Gem.location_of_caller.join ':'}:Warning: Kernel.gem no longer "\ + "accepts a Gem::Dependency object, please pass the name "\ + "and requirements directly" + end + + requirements = gem_name.requirement + gem_name = gem_name.name + end + + spec = Gem::Dependency.new(gem_name, *requirements).to_spec + spec.activate if spec + end + + private :gem + +end diff --git a/lib/rubygems/core_ext/kernel_require.rb b/lib/rubygems/core_ext/kernel_require.rb new file mode 100755 index 0000000000..e6dfce644f --- /dev/null +++ b/lib/rubygems/core_ext/kernel_require.rb @@ -0,0 +1,119 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +module Kernel + + if defined?(gem_original_require) then + # Ruby ships with a custom_require, override its require + remove_method :require + else + ## + # The Kernel#require from before RubyGems was loaded. + + alias gem_original_require require + private :gem_original_require + end + + ## + # When RubyGems is required, Kernel#require is replaced with our own which + # is capable of loading gems on demand. + # + # When you call require 'x', this is what happens: + # * If the file can be loaded from the existing Ruby loadpath, it + # is. + # * Otherwise, installed gems are searched for a file that matches. + # If it's found in gem 'y', that gem is activated (added to the + # loadpath). + # + # The normal require functionality of returning false if + # that file has already been loaded is preserved. + + def require path + spec = Gem.find_unresolved_default_spec(path) + if spec + Gem.remove_unresolved_default_spec(spec) + gem(spec.name) + end + + # If there are no unresolved deps, then we can use just try + # normal require handle loading a gem from the rescue below. + + if Gem::Specification.unresolved_deps.empty? then + return gem_original_require(path) + end + + # If +path+ is for a gem that has already been loaded, don't + # bother trying to find it in an unresolved gem, just go straight + # to normal require. + #-- + # TODO request access to the C implementation of this to speed up RubyGems + + spec = Gem::Specification.find { |s| + s.activated? and s.contains_requirable_file? path + } + + return gem_original_require(path) if spec + + # Attempt to find +path+ in any unresolved gems... + + found_specs = Gem::Specification.find_in_unresolved path + + # If there are no directly unresolved gems, then try and find +path+ + # in any gems that are available via the currently unresolved gems. + # For example, given: + # + # a => b => c => d + # + # If a and b are currently active with c being unresolved and d.rb is + # requested, then find_in_unresolved_tree will find d.rb in d because + # it's a dependency of c. + # + if found_specs.empty? then + found_specs = Gem::Specification.find_in_unresolved_tree path + + found_specs.each do |found_spec| + found_spec.activate + end + + # We found +path+ directly in an unresolved gem. Now we figure out, of + # the possible found specs, which one we should activate. + else + + # Check that all the found specs are just different + # versions of the same gem + names = found_specs.map(&:name).uniq + + if names.size > 1 then + raise Gem::LoadError, "#{path} found in multiple gems: #{names.join ', '}" + end + + # Ok, now find a gem that has no conflicts, starting + # at the highest version. + valid = found_specs.select { |s| s.conflicts.empty? }.last + + unless valid then + le = Gem::LoadError.new "unable to find a version of '#{names.first}' to activate" + le.name = names.first + raise le + end + + valid.activate + end + + gem_original_require path + rescue LoadError => load_error + if load_error.message.start_with?("Could not find") or + (load_error.message.end_with?(path) and Gem.try_activate(path)) then + return gem_original_require(path) + end + + raise load_error + end + + private :require + +end + diff --git a/lib/rubygems/custom_require.rb b/lib/rubygems/custom_require.rb deleted file mode 100644 index c813e3aaa2..0000000000 --- a/lib/rubygems/custom_require.rb +++ /dev/null @@ -1,69 +0,0 @@ -#-- -# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. -# All rights reserved. -# See LICENSE.txt for permissions. -#++ - -module Kernel - - if defined?(gem_original_require) then - # Ruby ships with a custom_require, override its require - remove_method :require - else - ## - # The Kernel#require from before RubyGems was loaded. - - alias gem_original_require require - private :gem_original_require - end - - ## - # When RubyGems is required, Kernel#require is replaced with our own which - # is capable of loading gems on demand. - # - # When you call require 'x', this is what happens: - # * If the file can be loaded from the existing Ruby loadpath, it - # is. - # * Otherwise, installed gems are searched for a file that matches. - # If it's found in gem 'y', that gem is activated (added to the - # loadpath). - # - # The normal require functionality of returning false if - # that file has already been loaded is preserved. - - def require path - if Gem.unresolved_deps.empty? then - gem_original_require path - else - spec = Gem::Specification.find { |s| - s.activated? and s.contains_requirable_file? path - } - - unless spec then - found_specs = Gem::Specification.find_in_unresolved path - unless found_specs.empty? then - found_specs = [found_specs.last] - else - found_specs = Gem::Specification.find_in_unresolved_tree path - end - - found_specs.each do |found_spec| - found_spec.activate - end - end - - return gem_original_require path - end - rescue LoadError => load_error - if load_error.message.start_with?("Could not find") or - (load_error.message.end_with?(path) and Gem.try_activate(path)) then - return gem_original_require(path) - end - - raise load_error - end - - private :require - -end - diff --git a/lib/rubygems/defaults.rb b/lib/rubygems/defaults.rb index d6732adbfa..e32131b9d4 100644 --- a/lib/rubygems/defaults.rb +++ b/lib/rubygems/defaults.rb @@ -1,8 +1,8 @@ module Gem - - # TODO: move this whole file back into rubygems.rb + DEFAULT_HOST = "https://rubygems.org" @post_install_hooks ||= [] + @done_installing_hooks ||= [] @post_uninstall_hooks ||= [] @pre_uninstall_hooks ||= [] @pre_install_hooks ||= [] @@ -61,7 +61,7 @@ module Gem # Default gem load path def self.default_path - if File.exist? Gem.user_home then + if Gem.user_home && File.exist?(Gem.user_home) then [user_dir, default_dir] else [default_dir] @@ -93,24 +93,6 @@ module Gem end end - ## - # The default system-wide source info cache directory - - def self.default_system_source_cache_dir - File.join(Gem.dir, 'source_cache') - end - - ## - # The default user-specific source info cache directory - - def self.default_user_source_cache_dir - # - # NOTE Probably an argument for moving this to per-ruby supported dirs like - # user_dir - # - File.join(Gem.user_home, '.gem', 'source_cache') - end - ## # A wrapper around RUBY_ENGINE const that may not be defined diff --git a/lib/rubygems/dependency.rb b/lib/rubygems/dependency.rb index 0caf65c6c4..217189bf8e 100644 --- a/lib/rubygems/dependency.rb +++ b/lib/rubygems/dependency.rb @@ -1,8 +1,8 @@ -require "rubygems/requirement" - ## # The Dependency class holds a Gem name and a Gem::Requirement. +require "rubygems/requirement" + class Gem::Dependency ## @@ -11,6 +11,9 @@ class Gem::Dependency # When this list is updated, be sure to change # Gem::Specification::CURRENT_SPECIFICATION_VERSION as well. + # REFACTOR: This type of constant, TYPES, indicates we might want + # two classes, used via inheretance or duck typing. + TYPES = [ :development, :runtime, @@ -32,18 +35,23 @@ class Gem::Dependency # :runtime. def initialize name, *requirements - if Regexp === name then + case name + when String then # ok + when Regexp then msg = ["NOTE: Dependency.new w/ a regexp is deprecated.", "Dependency.new called from #{Gem.location_of_caller.join(":")}"] warn msg.join("\n") unless Gem::Deprecate.skip + else + raise ArgumentError, + "dependency name must be a String, was #{name.inspect}" end type = Symbol === requirements.last ? requirements.pop : :runtime requirements = requirements.first if 1 == requirements.length # unpack unless TYPES.include? type - raise ArgumentError, "Valid types are #{TYPES.inspect}, " - + "not #{type.inspect}" + raise ArgumentError, "Valid types are #{TYPES.inspect}, " + + "not #{type.inspect}" end @name = name @@ -66,8 +74,13 @@ class Gem::Dependency end def inspect # :nodoc: - "<%s type=%p name=%p requirements=%p>" % - [self.class, self.type, self.name, requirement.to_s] + if @prerelease + "<%s type=%p name=%p requirements=%p prerelease=ok>" % + [self.class, self.type, self.name, requirement.to_s] + else + "<%s type=%p name=%p requirements=%p>" % + [self.class, self.type, self.name, requirement.to_s] + end end ## @@ -77,6 +90,14 @@ class Gem::Dependency @prerelease || requirement.prerelease? end + ## + # Is this dependency simply asking for the latest version + # of a gem? + + def latest_version? + @requirement.none? + end + def pretty_print q # :nodoc: q.group 1, 'Gem::Dependency.new(', ')' do q.pp name @@ -113,6 +134,8 @@ class Gem::Dependency # Children, define explicit marshal and unmarshal behavior for # public classes. Marshal formats are part of your public API. + # REFACTOR: See above + if defined?(@version_requirement) && @version_requirement version = @version_requirement.instance_variable_get :@version @version_requirement = nil @@ -122,6 +145,7 @@ class Gem::Dependency @requirement = @version_requirements if defined?(@version_requirements) end + # DOC: this method needs documentation or :nodoc''d def requirements_list requirement.as_list end @@ -179,13 +203,24 @@ class Gem::Dependency requirement.satisfied_by? version end - def match? name, version + # DOC: this method needs either documented or :nodoc'd + + def match? obj, version=nil + if !version + name = obj.name + version = obj.version + else + name = obj + end + return false unless self.name === name return true if requirement.none? requirement.satisfied_by? Gem::Version.new(version) end + # DOC: this method needs either documented or :nodoc'd + def matches_spec? spec return false unless name === spec.name return true if requirement.none? @@ -212,6 +247,8 @@ class Gem::Dependency self.class.new name, self_req.as_list.concat(other_req.as_list) end + # DOC: this method needs either documented or :nodoc'd + def matching_specs platform_only = false matches = Gem::Specification.find_all { |spec| self.name === spec.name and # TODO: == instead of === @@ -234,14 +271,26 @@ class Gem::Dependency @requirement.specific? end + # DOC: this method needs either documented or :nodoc'd + def to_specs matches = matching_specs true # TODO: check Gem.activated_spec[self.name] in case matches falls outside if matches.empty? then - specs = Gem::Specification.all_names.join ", " - error = Gem::LoadError.new "Could not find #{name} (#{requirement}) amongst [#{specs}]" + specs = Gem::Specification.find_all { |s| + s.name == name + }.map { |x| x.full_name } + + if specs.empty? + total = Gem::Specification.to_a.size + error = Gem::LoadError.new \ + "Could not find '#{name}' (#{requirement}) among #{total} total gem(s)" + else + error = Gem::LoadError.new \ + "Could not find '#{name}' (#{requirement}) - did find: [#{specs.join ','}]" + end error.name = self.name error.requirement = self.requirement raise error @@ -252,6 +301,8 @@ class Gem::Dependency matches end + # DOC: this method needs either documented or :nodoc'd + def to_spec matches = self.to_specs diff --git a/lib/rubygems/dependency_installer.rb b/lib/rubygems/dependency_installer.rb index 6303e8e9ac..a2665633fc 100644 --- a/lib/rubygems/dependency_installer.rb +++ b/lib/rubygems/dependency_installer.rb @@ -1,8 +1,12 @@ require 'rubygems' require 'rubygems/dependency_list' +require 'rubygems/package' require 'rubygems/installer' require 'rubygems/spec_fetcher' require 'rubygems/user_interaction' +require 'rubygems/source_local' +require 'rubygems/source_specific_file' +require 'rubygems/available_set' ## # Installs a gem along with all its dependencies from local and remote gems. @@ -14,8 +18,14 @@ class Gem::DependencyInstaller attr_reader :gems_to_install attr_reader :installed_gems + ## + # Documentation types. For use by the Gem.done_installing hook + + attr_reader :document + DEFAULT_OPTIONS = { :env_shebang => false, + :document => %w[ri], :domain => :both, # HACK dup :force => false, :format_executable => false, # HACK dup @@ -23,7 +33,8 @@ class Gem::DependencyInstaller :prerelease => false, :security_policy => nil, # HACK NoSecurity requires OpenSSL. AlmostNo? Low? :wrappers => true, - } + :build_docs_in_background => false, + }.freeze ## # Creates a new installer instance. @@ -47,6 +58,7 @@ class Gem::DependencyInstaller if options[:install_dir] then @gem_home = options[:install_dir] + # HACK shouldn't change the global settings Gem::Specification.dirs = @gem_home Gem.ensure_gem_subdirectories @gem_home options[:install_dir] = @gem_home # FIX: because we suck and reuse below @@ -55,7 +67,9 @@ class Gem::DependencyInstaller options = DEFAULT_OPTIONS.merge options @bin_dir = options[:bin_dir] + @dev_shallow = options[:dev_shallow] @development = options[:development] + @document = options[:document] @domain = options[:domain] @env_shebang = options[:env_shebang] @force = options[:force] @@ -65,8 +79,14 @@ class Gem::DependencyInstaller @security_policy = options[:security_policy] @user_install = options[:user_install] @wrappers = options[:wrappers] + @build_docs_in_background = options[:build_docs_in_background] + + # Indicates that we should not try to update any deps unless + # we absolutely must. + @minimal_deps = options[:minimal_deps] @installed_gems = [] + @toplevel_specs = nil @install_dir = options[:install_dir] || Gem.dir @cache_dir = options[:cache_dir] || @install_dir @@ -76,6 +96,24 @@ class Gem::DependencyInstaller @errors = nil end + attr_reader :errors + + ## + # Indicated, based on the requested domain, if local + # gems should be considered. + + def consider_local? + @domain == :both or @domain == :local + end + + ## + # Indicated, based on the requested domain, if remote + # gems should be considered. + + def consider_remote? + @domain == :both or @domain == :remote + end + ## # Returns a list of pairs of gemspecs and source_uris that match # Gem::Dependency +dep+ from both local (Dir.pwd) and remote (Gem.sources) @@ -83,35 +121,34 @@ class Gem::DependencyInstaller # local gems preferred over remote gems. def find_gems_with_sources(dep) - # Reset the errors - @errors = nil - gems_and_sources = [] + set = Gem::AvailableSet.new + + if consider_local? + sl = Gem::Source::Local.new - if @domain == :both or @domain == :local then - Dir[File.join(Dir.pwd, "#{dep.name}-[0-9]*.gem")].each do |gem_file| - spec = Gem::Format.from_file_by_path(gem_file).spec - gems_and_sources << [spec, gem_file] if spec.name == dep.name + if spec = sl.find_gem(dep.name) + if dep.matches_spec? spec + set.add spec, sl + end end end - if @domain == :both or @domain == :remote then + if consider_remote? begin - # REFACTOR: all = dep.requirement.needs_all? - requirements = dep.requirement.requirements.map do |req, ver| - req - end - - all = !dep.prerelease? && - # we only need latest if there's one requirement and it is - # guaranteed to match the newest specs - (requirements.length > 1 or - (requirements.first != ">=" and requirements.first != ">")) + found, errors = Gem::SpecFetcher.fetcher.spec_for_dependency dep - found, @errors = Gem::SpecFetcher.fetcher.fetch_with_errors dep, all, true, dep.prerelease? + if @errors + @errors += errors + else + @errors = errors + end - gems_and_sources.push(*found) + set << found rescue Gem::RemoteFetcher::FetchError => e + # FIX if there is a problem talking to the network, we either need to always tell + # the user (no really_verbose) or fail hard, not silently tell them that we just + # couldn't find their requested gem. if Gem.configuration.really_verbose then say "Error fetching remote data:\t\t#{e.message}" say "Falling back to local-only install" @@ -120,9 +157,7 @@ class Gem::DependencyInstaller end end - gems_and_sources.sort_by do |gem, source| - [gem, source =~ /^http:\/\// ? 0 : 1] # local gems win - end + set end ## @@ -130,17 +165,22 @@ class Gem::DependencyInstaller # remote sources unless the ignore_dependencies was given. def gather_dependencies - specs = @specs_and_sources.map { |spec,_| spec } + specs = @available.all_specs # these gems were listed by the user, always install them keep_names = specs.map { |spec| spec.full_name } + if @dev_shallow + @toplevel_specs = keep_names + end + dependency_list = Gem::DependencyList.new @development dependency_list.add(*specs) to_do = specs.dup - add_found_dependencies to_do, dependency_list unless @ignore_dependencies + # REFACTOR maybe abstract away using Gem::Specification.include? so + # that this isn't dependent only on the currently installed gems dependency_list.specs.reject! { |spec| not keep_names.include?(spec.full_name) and Gem::Specification.include?(spec) @@ -162,32 +202,43 @@ class Gem::DependencyInstaller until to_do.empty? do spec = to_do.shift + + # HACK why is spec nil? next if spec.nil? or seen[spec.name] seen[spec.name] = true deps = spec.runtime_dependencies - deps |= spec.development_dependencies if @development + + if @development + if @dev_shallow + if @toplevel_specs.include? spec.full_name + deps |= spec.development_dependencies + end + else + deps |= spec.development_dependencies + end + end deps.each do |dep| dependencies[dep.name] = dependencies[dep.name].merge dep - results = find_gems_with_sources(dep).reverse + if @minimal_deps + next if Gem::Specification.any? do |installed_spec| + dep.name == installed_spec.name and + dep.requirement.satisfied_by? installed_spec.version + end + end - results.reject! do |dep_spec,| - to_do.push dep_spec + results = find_gems_with_sources(dep) - # already locally installed - Gem::Specification.any? do |installed_spec| - dep.name == installed_spec.name and - dep.requirement.satisfied_by? installed_spec.version - end + results.sorted.each do |t| + to_do.push t.spec end - results.each do |dep_spec, source_uri| - @specs_and_sources << [dep_spec, source_uri] + results.remove_installed! dep - dependency_list.add dep_spec - end + @available << results + results.inject_into_list dependency_list end end @@ -202,42 +253,36 @@ class Gem::DependencyInstaller def find_spec_by_name_and_version(gem_name, version = Gem::Requirement.default, prerelease = false) - spec_and_source = nil - glob = if File::ALT_SEPARATOR then - gem_name.gsub File::ALT_SEPARATOR, File::SEPARATOR - else - gem_name - end + set = Gem::AvailableSet.new - local_gems = Dir["#{glob}*"].sort.reverse + if consider_local? + if File.exists? gem_name + src = Gem::Source::SpecificFile.new(gem_name) + set.add src.spec, src + else + local = Gem::Source::Local.new - local_gems.each do |gem_file| - next unless gem_file =~ /gem$/ - begin - spec = Gem::Format.from_file_by_path(gem_file).spec - spec_and_source = [spec, gem_file] - break - rescue SystemCallError, Gem::Package::FormatError + if s = local.find_gem(gem_name, version) + set.add s, local + end end end - unless spec_and_source then + if set.empty? dep = Gem::Dependency.new gem_name, version + # HACK Dependency objects should be immutable dep.prerelease = true if prerelease - spec_and_sources = find_gems_with_sources(dep).reverse - spec_and_source = spec_and_sources.find { |spec, source| - Gem::Platform.match spec.platform - } + + set = find_gems_with_sources(dep) + set.match_platform! end - if spec_and_source.nil? then - raise Gem::GemNotFoundException.new( - "Could not find a valid gem '#{gem_name}' (#{version}) locally or in a repository", - gem_name, version, @errors) + if set.empty? + raise Gem::SpecificGemNotFoundException.new(gem_name, version, @errors) end - @specs_and_sources = [spec_and_source] + @available = set end ## @@ -258,33 +303,49 @@ class Gem::DependencyInstaller if String === dep_or_name then find_spec_by_name_and_version dep_or_name, version, @prerelease else - dep_or_name.prerelease = @prerelease - @specs_and_sources = [find_gems_with_sources(dep_or_name).last] + dep = dep_or_name.dup + dep.prerelease = @prerelease + @available = find_gems_with_sources(dep).pick_best! end @installed_gems = [] gather_dependencies + # REFACTOR is the last gem always the one that the user requested? + # This code assumes that but is that actually validated by the code? + last = @gems_to_install.size - 1 @gems_to_install.each_with_index do |spec, index| + # REFACTOR more current spec set hardcoding, should be abstracted? next if Gem::Specification.include?(spec) and index != last # TODO: make this sorta_verbose so other users can benefit from it say "Installing gem #{spec.full_name}" if Gem.configuration.really_verbose - _, source_uri = @specs_and_sources.assoc spec + source = @available.source_for spec + begin - local_gem_path = Gem::RemoteFetcher.fetcher.download spec, source_uri, - @cache_dir + # REFACTOR make the fetcher to use configurable + local_gem_path = source.download spec, @cache_dir rescue Gem::RemoteFetcher::FetchError + # TODO I doubt all fetch errors are recoverable, we should at least + # report the errors probably. next if @force raise end + if @development + if @dev_shallow + is_dev = @toplevel_specs.include? spec.full_name + else + is_dev = true + end + end + inst = Gem::Installer.new local_gem_path, :bin_dir => @bin_dir, - :development => @development, + :development => is_dev, :env_shebang => @env_shebang, :force => @force, :format_executable => @format_executable, @@ -299,6 +360,33 @@ class Gem::DependencyInstaller @installed_gems << spec end + # Since this is currently only called for docs, we can be lazy and just say + # it's documentation. Ideally the hook adder could decide whether to be in + # the background or not, and what to call it. + in_background "Installing documentation" do + start = Time.now + Gem.done_installing_hooks.each do |hook| + hook.call self, @installed_gems + end + finish = Time.now + say "Done installing documentation for #{@installed_gems.map(&:name).join(', ')} (#{(finish-start).to_i} sec)." + end unless Gem.done_installing_hooks.empty? + @installed_gems end + + def in_background what + fork_happened = false + if @build_docs_in_background and Process.respond_to?(:fork) + begin + Process.fork do + yield + end + fork_happened = true + say "#{what} in a background process." + rescue NotImplementedError + end + end + yield unless fork_happened + end end diff --git a/lib/rubygems/dependency_list.rb b/lib/rubygems/dependency_list.rb index 9f1da9166c..c147efc1ca 100644 --- a/lib/rubygems/dependency_list.rb +++ b/lib/rubygems/dependency_list.rb @@ -10,6 +10,10 @@ require 'rubygems/deprecate' ## # Gem::DependencyList is used for installing and uninstalling gems in the # correct order to avoid conflicts. +#-- +# TODO: It appears that all but topo-sort functionality is being duplicated +# (or is planned to be duplicated) elsewhere in rubygems. Is the majority of +# this class necessary anymore? Especially #ok?, #why_not_ok? class Gem::DependencyList attr_reader :specs @@ -27,19 +31,10 @@ class Gem::DependencyList def self.from_specs list = new - list.add(*Gem::Specification.map) + list.add(*Gem::Specification.to_a) list end - ## - # Creates a DependencyList from a Gem::SourceIndex +source_index+ - - def self.from_source_index(ignored=nil) - warn "NOTE: DependencyList.from_source_index ignores it's arg" if ignored - - from_specs - end - ## # Creates a new DependencyList. If +development+ is true, development # dependencies will be included. @@ -143,7 +138,7 @@ class Gem::DependencyList # If removing the gemspec creates breaks a currently ok dependency, then it # is NOT ok to remove the gemspec. - def ok_to_remove?(full_name) + def ok_to_remove?(full_name, check_dev=true) gem_to_remove = find_name full_name siblings = @specs.find_all { |s| @@ -154,7 +149,9 @@ class Gem::DependencyList deps = [] @specs.each do |spec| - spec.dependencies.each do |dep| + check = check_dev ? spec.dependencies : spec.runtime_dependencies + + check.each do |dep| deps << dep if gem_to_remove.satisfies_requirement?(dep) end end @@ -213,7 +210,7 @@ class Gem::DependencyList @specs.each(&block) end - def tsort_each_child(node, &block) + def tsort_each_child(node) specs = @specs.sort.reverse dependencies = node.runtime_dependencies @@ -242,11 +239,6 @@ class Gem::DependencyList def active_count(specs, ignored) specs.count { |spec| ignored[spec.full_name].nil? } end -end -class Gem::DependencyList - class << self - extend Gem::Deprecate - deprecate :from_source_index, "from_specs", 2011, 11 - end end + diff --git a/lib/rubygems/dependency_resolver.rb b/lib/rubygems/dependency_resolver.rb new file mode 100644 index 0000000000..b841674d43 --- /dev/null +++ b/lib/rubygems/dependency_resolver.rb @@ -0,0 +1,562 @@ +require 'rubygems' +require 'rubygems/dependency' +require 'rubygems/exceptions' + +require 'uri' +require 'net/http' + +module Gem + + # Raised when a DependencyConflict reaches the toplevel. + # Indicates which dependencies were incompatible. + # + class DependencyResolutionError < Gem::Exception + def initialize(conflict) + @conflict = conflict + a, b = conflicting_dependencies + + super "unable to resolve conflicting dependencies '#{a}' and '#{b}'" + end + + attr_reader :conflict + + def conflicting_dependencies + @conflict.conflicting_dependencies + end + end + + # Raised when a dependency requests a gem for which there is + # no spec. + # + class UnsatisfiableDepedencyError < Gem::Exception + def initialize(dep) + super "unable to find any gem matching dependency '#{dep}'" + + @dependency = dep + end + + attr_reader :dependency + end + + # Raised when dependencies conflict and create the inability to + # find a valid possible spec for a request. + # + class ImpossibleDependenciesError < Gem::Exception + def initialize(request, conflicts) + s = conflicts.size == 1 ? "" : "s" + super "detected #{conflicts.size} conflict#{s} with dependency '#{request.dependency}'" + @request = request + @conflicts = conflicts + end + + def dependency + @request.dependency + end + + attr_reader :conflicts + end + + # Given a set of Gem::Dependency objects as +needed+ and a way + # to query the set of available specs via +set+, calculates + # a set of ActivationRequest objects which indicate all the specs + # that should be activated to meet the all the requirements. + # + class DependencyResolver + + # Represents a specification retrieved via the rubygems.org + # API. This is used to avoid having to load the full + # Specification object when all we need is the name, version, + # and dependencies. + # + class APISpecification + def initialize(set, api_data) + @set = set + @name = api_data[:name] + @version = Gem::Version.new api_data[:number] + @dependencies = api_data[:dependencies].map do |name, ver| + Gem::Dependency.new name, ver.split(/\s*,\s*/) + end + end + + attr_reader :name, :version, :dependencies + + def full_name + "#{@name}-#{@version}" + end + end + + # The global rubygems pool, available via the rubygems.org API. + # Returns instances of APISpecification. + # + class APISet + def initialize + @data = Hash.new { |h,k| h[k] = [] } + end + + # Return data for all versions of the gem +name+. + # + def versions(name) + if @data.key?(name) + return @data[name] + end + + u = URI.parse "http://rubygems.org/api/v1/dependencies?gems=#{name}" + str = Net::HTTP.get(u) + + Marshal.load(str).each do |ver| + @data[ver[:name]] << ver + end + + @data[name] + end + + # Return an array of APISpecification objects matching + # DependencyRequest +req+. + # + def find_all(req) + res = [] + + versions(req.name).each do |ver| + if req.dependency.match? req.name, ver[:number] + res << APISpecification.new(self, ver) + end + end + + res + end + + # A hint run by the resolver to allow the Set to fetch + # data for DependencyRequests +reqs+. + # + def prefetch(reqs) + names = reqs.map { |r| r.dependency.name } + needed = names.find_all { |d| !@data.key?(d) } + + return if needed.empty? + + u = URI.parse "http://rubygems.org/api/v1/dependencies?gems=#{needed.join ','}" + str = Net::HTTP.get(u) + + Marshal.load(str).each do |ver| + @data[ver[:name]] << ver + end + end + end + + # Represents a possible Specification object returned + # from IndexSet. Used to delay needed to download full + # Specification objects when only the +name+ and +version+ + # are needed. + # + class IndexSpecification + def initialize(set, name, version, source, plat) + @set = set + @name = name + @version = version + @source = source + @platform = plat + + @spec = nil + end + + attr_reader :name, :version, :source + + def full_name + "#{@name}-#{@version}" + end + + def spec + @spec ||= @set.load_spec(@name, @version, @source) + end + + def dependencies + spec.dependencies + end + end + + # The global rubygems pool represented via the traditional + # source index. + # + class IndexSet + def initialize + @f = Gem::SpecFetcher.fetcher + + @all = Hash.new { |h,k| h[k] = [] } + + list, _ = @f.available_specs(:released) + list.each do |uri, specs| + specs.each do |n| + @all[n.name] << [uri, n] + end + end + + @specs = {} + end + + # Return an array of IndexSpecification objects matching + # DependencyRequest +req+. + # + def find_all(req) + res = [] + + name = req.dependency.name + + @all[name].each do |uri, n| + if req.dependency.match? n + res << IndexSpecification.new(self, n.name, n.version, + uri, n.platform) + end + end + + res + end + + # No prefetching needed since we load the whole index in + # initially. + # + def prefetch(gems) + end + + # Called from IndexSpecification to get a true Specification + # object. + # + def load_spec(name, ver, source) + key = "#{name}-#{ver}" + @specs[key] ||= source.fetch_spec(Gem::NameTuple.new(name, ver)) + end + end + + # A set which represents the installed gems. Respects + # all the normal settings that control where to look + # for installed gems. + # + class CurrentSet + def find_all(req) + req.dependency.matching_specs + end + + def prefetch(gems) + end + end + + # Create DependencyResolver object which will resolve + # the tree starting with +needed+ Depedency objects. + # + # +set+ is an object that provides where to look for + # specifications to satisify the Dependencies. This + # defaults to IndexSet, which will query rubygems.org. + # + def initialize(needed, set=IndexSet.new) + @set = set || IndexSet.new # Allow nil to mean IndexSet + @needed = needed + + @conflicts = nil + end + + # Provide a DependencyResolver that queries only against + # the already installed gems. + # + def self.for_current_gems(needed) + new needed, CurrentSet.new + end + + # Contains all the conflicts encountered while doing resolution + # + attr_reader :conflicts + + # Proceed with resolution! Returns an array of ActivationRequest + # objects. + # + def resolve + @conflicts = [] + + needed = @needed.map { |n| DependencyRequest.new(n, nil) } + + res = resolve_for needed, [] + + if res.kind_of? DependencyConflict + raise DependencyResolutionError.new(res) + end + + res + end + + # Used internally to indicate that a dependency conflicted + # with a spec that would be activated. + # + class DependencyConflict + def initialize(dependency, activated, failed_dep=dependency) + @dependency = dependency + @activated = activated + @failed_dep = failed_dep + end + + attr_reader :dependency, :activated + + # Return the Specification that listed the dependency + # + def requester + @failed_dep.requester + end + + def for_spec?(spec) + @dependency.name == spec.name + end + + # Return the 2 dependency objects that conflicted + # + def conflicting_dependencies + [@failed_dep.dependency, @activated.request.dependency] + end + end + + # Used Internally. Wraps a Depedency object to also track + # which spec contained the Dependency. + # + class DependencyRequest + def initialize(dep, act) + @dependency = dep + @requester = act + end + + attr_reader :dependency, :requester + + def name + @dependency.name + end + + def matches_spec?(spec) + @dependency.matches_spec? spec + end + + def to_s + @dependency.to_s + end + + def ==(other) + case other + when Dependency + @dependency == other + when DependencyRequest + @dependency == other.dep && @requester == other.requester + else + false + end + end + end + + # Specifies a Specification object that should be activated. + # Also contains a dependency that was used to introduce this + # activation. + # + class ActivationRequest + def initialize(spec, req, others_possible=true) + @spec = spec + @request = req + @others_possible = others_possible + end + + attr_reader :spec, :request + + # Indicate if this activation is one of a set of possible + # requests for the same Dependency request. + # + def others_possible? + @others_possible + end + + # Return the ActivationRequest that contained the dependency + # that we were activated for. + # + def parent + @request.requester + end + + def name + @spec.name + end + + def full_name + @spec.full_name + end + + def version + @spec.version + end + + def full_spec + Gem::Specification === @spec ? @spec : @spec.spec + end + + def download(path) + if @spec.respond_to? :source + source = @spec.source + else + source = Gem.sources.first + end + + source.download full_spec, path + end + + def ==(other) + case other + when Gem::Specification + @spec == other + when ActivationRequest + @spec == other.spec && @request == other.request + else + false + end + end + + ## + # Indicates if the requested gem has already been installed. + + def installed? + this_spec = full_spec + + Gem::Specification.any? do |s| + s == this_spec + end + end + end + + def requests(s, act) + reqs = [] + s.dependencies.each do |d| + next unless d.type == :runtime + reqs << DependencyRequest.new(d, act) + end + + @set.prefetch(reqs) + + reqs + end + + # The meat of the algorithm. Given +needed+ DependencyRequest objects + # and +specs+ being a list to ActivationRequest, calculate a new list + # of ActivationRequest objects. + # + def resolve_for(needed, specs) + until needed.empty? + dep = needed.shift + + # If there is already a spec activated for the requested name... + if existing = specs.find { |s| dep.name == s.name } + + # then we're done since this new dep matches the + # existing spec. + next if dep.matches_spec? existing + + # There is a conflict! We return the conflict + # object which will be seen by the caller and be + # handled at the right level. + + # If the existing activation indicates that there + # are other possibles for it, then issue the conflict + # on the dep for the activation itself. Otherwise, issue + # it on the requester's request itself. + # + if existing.others_possible? + conflict = DependencyConflict.new(dep, existing) + else + depreq = existing.request.requester.request + conflict = DependencyConflict.new(depreq, existing, dep) + end + @conflicts << conflict + + return conflict + end + + # Get a list of all specs that satisfy dep + possible = @set.find_all(dep) + + case possible.size + when 0 + # If there are none, then our work here is done. + raise UnsatisfiableDepedencyError.new(dep) + when 1 + # If there is one, then we just add it to specs + # and process the specs dependencies by adding + # them to needed. + + spec = possible.first + act = ActivationRequest.new(spec, dep, false) + + specs << act + + # Put the deps for at the beginning of needed + # rather than the end to match the depth first + # searching done by the multiple case code below. + # + # This keeps the error messages consistent. + needed = requests(spec, act) + needed + else + # There are multiple specs for this dep. This is + # the case that this class is built to handle. + + # Sort them so that we try the highest versions + # first. + possible = possible.sort_by { |s| s.version } + + # We track the conflicts seen so that we can report them + # to help the user figure out how to fix the situation. + conflicts = [] + + # To figure out which to pick, we keep resolving + # given each one being activated and if there isn't + # a conflict, we know we've found a full set. + # + # We use an until loop rather than #reverse_each + # to keep the stack short since we're using a recursive + # algorithm. + # + until possible.empty? + s = possible.pop + + # Recursively call #resolve_for with this spec + # and add it's dependencies into the picture... + + act = ActivationRequest.new(s, dep) + + try = requests(s, act) + needed + + res = resolve_for(try, specs + [act]) + + # While trying to resolve these dependencies, there may + # be a conflict! + + if res.kind_of? DependencyConflict + # The conflict might be created not by this invocation + # but rather one up the stack, so if we can't attempt + # to resolve this conflict (conflict isn't with the spec +s+) + # then just return it so the caller can try to sort it out. + return res unless res.for_spec? s + + # Otherwise, this is a conflict that we can attempt to fix + conflicts << [s, res] + + # Optimization: + # + # Because the conflict indicates the dependency that trigger + # it, we can prune possible based on this new information. + # + # This cuts down on the number of iterations needed. + possible.delete_if { |x| !res.dependency.matches_spec? x } + else + # No conflict, return the specs + return res + end + end + + # We tried all possibles and nothing worked, so we let the user + # know and include as much information about the problem since + # the user is going to have to take action to fix this. + raise ImpossibleDependenciesError.new(dep, conflicts) + end + end + + specs + end + end +end diff --git a/lib/rubygems/deprecate.rb b/lib/rubygems/deprecate.rb index a78208ec24..274d6a5c12 100644 --- a/lib/rubygems/deprecate.rb +++ b/lib/rubygems/deprecate.rb @@ -20,51 +20,51 @@ # end # end -module Gem - module Deprecate +module Gem::Deprecate - def self.skip # :nodoc: - @skip ||= false - end - - def self.skip= v # :nodoc: - @skip = v - end + def self.skip # :nodoc: + @skip ||= false + end - ## - # Temporarily turn off warnings. Intended for tests only. + def self.skip= v # :nodoc: + @skip = v + end - def skip_during - Gem::Deprecate.skip, original = true, Gem::Deprecate.skip - yield - ensure - Gem::Deprecate.skip = original - end + ## + # Temporarily turn off warnings. Intended for tests only. - ## - # Simple deprecation method that deprecates +name+ by wrapping it up - # in a dummy method. It warns on each call to the dummy method - # telling the user of +repl+ (unless +repl+ is :none) and the - # year/month that it is planned to go away. + def skip_during + Gem::Deprecate.skip, original = true, Gem::Deprecate.skip + yield + ensure + Gem::Deprecate.skip = original + end - def deprecate name, repl, year, month - class_eval { - old = "_deprecated_#{name}" - alias_method old, name - define_method name do |*args, &block| # TODO: really works on 1.8.7? - klass = self.kind_of? Module - target = klass ? "#{self}." : "#{self.class}#" - msg = [ "NOTE: #{target}#{name} is deprecated", - repl == :none ? " with no replacement" : ", use #{repl}", - ". It will be removed on or after %4d-%02d-01." % [year, month], - "\n#{target}#{name} called from #{Gem.location_of_caller.join(":")}", - ] - warn "#{msg.join}." unless Gem::Deprecate.skip - send old, *args, &block - end - } - end + ## + # Simple deprecation method that deprecates +name+ by wrapping it up + # in a dummy method. It warns on each call to the dummy method + # telling the user of +repl+ (unless +repl+ is :none) and the + # year/month that it is planned to go away. - module_function :deprecate, :skip_during + def deprecate name, repl, year, month + class_eval { + old = "_deprecated_#{name}" + alias_method old, name + define_method name do |*args, &block| # TODO: really works on 1.8.7? + klass = self.kind_of? Module + target = klass ? "#{self}." : "#{self.class}#" + msg = [ "NOTE: #{target}#{name} is deprecated", + repl == :none ? " with no replacement" : "; use #{repl} instead", + ". It will be removed on or after %4d-%02d-01." % [year, month], + "\n#{target}#{name} called from #{Gem.location_of_caller.join(":")}", + ] + warn "#{msg.join}." unless Gem::Deprecate.skip + send old, *args, &block + end + } end + + module_function :deprecate, :skip_during + end + diff --git a/lib/rubygems/doc_manager.rb b/lib/rubygems/doc_manager.rb deleted file mode 100644 index 826f57d9dd..0000000000 --- a/lib/rubygems/doc_manager.rb +++ /dev/null @@ -1,243 +0,0 @@ -#-- -# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. -# All rights reserved. -# See LICENSE.txt for permissions. -#++ - -require 'rubygems' - -## -# The documentation manager generates RDoc and RI for RubyGems. - -class Gem::DocManager - - include Gem::UserInteraction - - @configured_args = [] - - def self.configured_args - @configured_args ||= [] - end - - def self.configured_args=(args) - case args - when Array - @configured_args = args - when String - @configured_args = args.split - end - end - - ## - # Load RDoc from a gem if it is available, otherwise from Ruby's stdlib - - def self.load_rdoc - begin - gem 'rdoc' - rescue Gem::LoadError - # use built-in RDoc - end - - begin - require 'rdoc/rdoc' - - @rdoc_version = if defined? RDoc::VERSION then - Gem::Version.new RDoc::VERSION - else - Gem::Version.new '1.0.1' # HACK parsing is hard - end - - rescue LoadError => e - raise Gem::DocumentError, - "ERROR: RDoc documentation generator not installed: #{e}" - end - end - - def self.rdoc_version - @rdoc_version - end - - ## - # Updates the RI cache for RDoc 2 if it is installed - - def self.update_ri_cache - load_rdoc rescue return - - return unless defined? RDoc::VERSION # RDoc 1 does not have VERSION - - require 'rdoc/ri/driver' - - options = { - :use_cache => true, - :use_system => true, - :use_site => true, - :use_home => true, - :use_gems => true, - :formatter => RDoc::RI::Formatter, - } - - RDoc::RI::Driver.new(options).class_cache - end - - ## - # Create a document manager for +spec+. +rdoc_args+ contains arguments for - # RDoc (template etc.) as a String. - - def initialize(spec, rdoc_args="") - require 'fileutils' - @spec = spec - @doc_dir = spec.doc_dir - @rdoc_args = rdoc_args.nil? ? [] : rdoc_args.split - end - - ## - # Is the RDoc documentation installed? - - def rdoc_installed? - File.exist?(File.join(@doc_dir, "rdoc")) - end - - ## - # Is the RI documentation installed? - - def ri_installed? - File.exist?(File.join(@doc_dir, "ri")) - end - - ## - # Generate the RI documents for this gem spec. - # - # Note that if both RI and RDoc documents are generated from the same - # process, the RI docs should be done first (a likely bug in RDoc will cause - # RI docs generation to fail if run after RDoc). - - def generate_ri - setup_rdoc - install_ri # RDoc bug, ri goes first - - FileUtils.mkdir_p @doc_dir unless File.exist?(@doc_dir) - end - - ## - # Generate the RDoc documents for this gem spec. - # - # Note that if both RI and RDoc documents are generated from the same - # process, the RI docs should be done first (a likely bug in RDoc will cause - # RI docs generation to fail if run after RDoc). - - def generate_rdoc - setup_rdoc - install_rdoc - - FileUtils.mkdir_p @doc_dir unless File.exist?(@doc_dir) - end - - ## - # Generate and install RDoc into the documentation directory - - def install_rdoc - rdoc_dir = File.join @doc_dir, 'rdoc' - - FileUtils.rm_rf rdoc_dir - - say "Installing RDoc documentation for #{@spec.full_name}..." - run_rdoc '--op', rdoc_dir - end - - ## - # Generate and install RI into the documentation directory - - def install_ri - ri_dir = File.join @doc_dir, 'ri' - - FileUtils.rm_rf ri_dir - - say "Installing ri documentation for #{@spec.full_name}..." - run_rdoc '--ri', '--op', ri_dir - end - - ## - # Run RDoc with +args+, which is an ARGV style argument list - - def run_rdoc(*args) - args << @spec.rdoc_options - args << self.class.configured_args - args << @spec.require_paths.clone - args << @spec.extra_rdoc_files - args << '--title' << "#{@spec.full_name} Documentation" - args << '--quiet' - args = args.flatten.map do |arg| arg.to_s end - - if self.class.rdoc_version >= Gem::Version.new('2.4.0') then - args.delete '--inline-source' - args.delete '--promiscuous' - args.delete '-p' - args.delete '--one-file' - # HACK more - end - - debug_args = args.dup - - r = RDoc::RDoc.new - - old_pwd = Dir.pwd - Dir.chdir @spec.full_gem_path - - say "rdoc #{args.join ' '}" if Gem.configuration.really_verbose - - begin - r.document args - rescue Errno::EACCES => e - dirname = File.dirname e.message.split("-")[1].strip - raise Gem::FilePermissionError.new(dirname) - rescue Interrupt => e - raise e - rescue Exception => ex - alert_error "While generating documentation for #{@spec.full_name}" - ui.errs.puts "... MESSAGE: #{ex}" - ui.errs.puts "... RDOC args: #{debug_args.join(' ')}" - ui.errs.puts "\t#{ex.backtrace.join "\n\t"}" if - Gem.configuration.backtrace - terminate_interaction 1 - ensure - Dir.chdir old_pwd - end - end - - def setup_rdoc - if File.exist?(@doc_dir) && !File.writable?(@doc_dir) then - raise Gem::FilePermissionError.new(@doc_dir) - end - - FileUtils.mkdir_p @doc_dir unless File.exist?(@doc_dir) - - self.class.load_rdoc - end - - ## - # Remove RDoc and RI documentation - - def uninstall_doc - base_dir = @spec.base_dir - raise Gem::FilePermissionError.new base_dir unless File.writable? base_dir - - # TODO: ok... that's twice... ugh - old_name = [ - @spec.name, @spec.version, @spec.original_platform].join '-' - - doc_dir = @spec.doc_dir - unless File.directory? doc_dir then - doc_dir = File.join File.dirname(doc_dir), old_name - end - - ri_dir = @spec.ri_dir - unless File.directory? ri_dir then - ri_dir = File.join File.dirname(ri_dir), old_name - end - - FileUtils.rm_rf doc_dir - FileUtils.rm_rf ri_dir - end - -end - diff --git a/lib/rubygems/errors.rb b/lib/rubygems/errors.rb index 950b34d744..29e732cb1d 100644 --- a/lib/rubygems/errors.rb +++ b/lib/rubygems/errors.rb @@ -1,35 +1,88 @@ -class Gem::ErrorReason; end - -# Generated when trying to lookup a gem to indicate that the gem -# was found, but that it isn't usable on the current platform. -# -# fetch and install read these and report them to the user to aid -# in figuring out why a gem couldn't be installed. +## +# This file contains all the various exceptions and other errors that are used +# inside of RubyGems. # -class Gem::PlatformMismatch < Gem::ErrorReason +# DOC: Confirm _all_ - attr_reader :name - attr_reader :version - attr_reader :platforms +module Gem + ## + # Raised when RubyGems is unable to load or activate a gem. Contains the + # name and version requirements of the gem that either conflicts with + # already activated gems or that RubyGems is otherwise unable to activate. - def initialize(name, version) - @name = name - @version = version - @platforms = [] - end + class LoadError < ::LoadError + # Name of gem + attr_accessor :name - def add_platform(platform) - @platforms << platform + # Version requirement of gem + attr_accessor :requirement end - def wordy - prefix = "Found #{@name} (#{@version})" + # FIX: does this need to exist? The subclass is the only other reference + # I can find. + class ErrorReason; end + + # Generated when trying to lookup a gem to indicate that the gem + # was found, but that it isn't usable on the current platform. + # + # fetch and install read these and report them to the user to aid + # in figuring out why a gem couldn't be installed. + # + class PlatformMismatch < ErrorReason + + ## + # the name of the gem + attr_reader :name + + ## + # the version + attr_reader :version + + ## + # The platforms that are mismatched + attr_reader :platforms + + def initialize(name, version) + @name = name + @version = version + @platforms = [] + end - if @platforms.size == 1 - "#{prefix}, but was for platform #{@platforms[0]}" - else - "#{prefix}, but was for platforms #{@platforms.join(' ,')}" + ## + # append a platform to the list of mismatched platforms. + # + # Platforms are added via this instead of injected via the constructor + # so that we can loop over a list of mismatches and just add them rather + # than perform some kind of calculation mismatch summary before creation. + def add_platform(platform) + @platforms << platform + end + + ## + # A wordy description of the error. + def wordy + "Found %s (%), but was for platform%s %s" % + [@name, + @version, + @platforms.size == 1 ? 's' : '', + @platforms.join(' ,')] end end + ## + # An error that indicates we weren't able to fetch some + # data from a source + + class SourceFetchProblem < ErrorReason + def initialize(source, error) + @source = source + @error = error + end + + attr_reader :source, :error + + def wordy + "Unable to download data from #{@source.uri} - #{@error.message}" + end + end end diff --git a/lib/rubygems/exceptions.rb b/lib/rubygems/exceptions.rb index 55d67f9125..ff389b320b 100644 --- a/lib/rubygems/exceptions.rb +++ b/lib/rubygems/exceptions.rb @@ -1,7 +1,14 @@ +# TODO: the documentation in here is terrible. +# +# Each exception needs a brief description and the scenarios where it is +# likely to be raised + ## # Base exception class for RubyGems. All exception raised by RubyGems are a # subclass of this one. -class Gem::Exception < RuntimeError; end +class Gem::Exception < RuntimeError + attr_accessor :source_exception +end class Gem::CommandLineError < Gem::Exception; end @@ -24,11 +31,18 @@ class Gem::EndOfYAMLException < Gem::Exception; end ## # Signals that a file permission error is preventing the user from -# installing in the requested directories. +# operating on the given directory. + class Gem::FilePermissionError < Gem::Exception - def initialize(path) - super("You don't have write permissions into the #{path} directory.") + + attr_reader :directory + + def initialize directory + @directory = directory + + super "You don't have write permissions for the #{directory} directory." end + end ## @@ -37,9 +51,12 @@ class Gem::FormatException < Gem::Exception attr_accessor :file_path end -class Gem::GemNotFoundException < Gem::Exception - def initialize(msg, name=nil, version=nil, errors=nil) - super msg +class Gem::GemNotFoundException < Gem::Exception; end + +class Gem::SpecificGemNotFoundException < Gem::GemNotFoundException + def initialize(name, version, errors=nil) + super "Could not find a valid gem '#{name}' (#{version}) locally or in a repository" + @name = name @version = version @errors = errors @@ -89,3 +106,4 @@ class Gem::SystemExitException < SystemExit end end + diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb index 5e518962ce..e0e7387d9c 100644 --- a/lib/rubygems/ext/builder.rb +++ b/lib/rubygems/ext/builder.rb @@ -16,7 +16,7 @@ class Gem::Ext::Builder raise Gem::InstallError, "Makefile not found:\n\n#{results.join "\n"}" end - mf = File.read('Makefile') + mf = Gem.read_binary 'Makefile' mf = mf.gsub(/^RUBYARCHDIR\s*=\s*\$[^$]*/, "RUBYARCHDIR = #{dest_path}") mf = mf.gsub(/^RUBYLIBDIR\s*=\s*\$[^$]*/, "RUBYLIBDIR = #{dest_path}") @@ -24,18 +24,14 @@ class Gem::Ext::Builder # try to find make program from Ruby configure arguments first RbConfig::CONFIG['configure_args'] =~ /with-make-prog\=(\w+)/ - make_program = $1 || ENV['make'] + make_program = $1 || ENV['MAKE'] || ENV['make'] unless make_program then make_program = (/mswin/ =~ RUBY_PLATFORM) ? 'nmake' : 'make' end ['', ' install'].each do |target| cmd = "#{make_program}#{target}" - results << cmd - results << `#{cmd} #{redirector}` - - raise Gem::InstallError, "make#{target} failed:\n\n#{results}" unless - $?.success? + run(cmd, results, "make#{target}") end end @@ -43,12 +39,20 @@ class Gem::Ext::Builder '2>&1' end - def self.run(command, results) - results << command - results << `#{command} #{redirector}` + def self.run(command, results, command_name = nil) + verbose = Gem.configuration.really_verbose + + if verbose + puts(command) + system(command) + else + results << command + results << `#{command} #{redirector}` + end unless $?.success? then - raise Gem::InstallError, "#{class_name} failed:\n\n#{results.join "\n"}" + results << "Building has failed. See above output for more information on the failure." if verbose + raise Gem::InstallError, "#{command_name || class_name} failed:\n\n#{results.join "\n"}" end end diff --git a/lib/rubygems/ext/configure_builder.rb b/lib/rubygems/ext/configure_builder.rb index c2087eb5ad..ef8b29e427 100644 --- a/lib/rubygems/ext/configure_builder.rb +++ b/lib/rubygems/ext/configure_builder.rb @@ -8,10 +8,10 @@ require 'rubygems/ext/builder' class Gem::Ext::ConfigureBuilder < Gem::Ext::Builder - def self.build(extension, directory, dest_path, results) + def self.build(extension, directory, dest_path, results, args=[]) unless File.exist?('Makefile') then cmd = "sh ./configure --prefix=#{dest_path}" - cmd << " #{Gem::Command.build_args.join ' '}" unless Gem::Command.build_args.empty? + cmd << " #{args.join ' '}" unless args.empty? run cmd, results end diff --git a/lib/rubygems/ext/ext_conf_builder.rb b/lib/rubygems/ext/ext_conf_builder.rb index b3d588dc9c..7ca322d3e5 100644 --- a/lib/rubygems/ext/ext_conf_builder.rb +++ b/lib/rubygems/ext/ext_conf_builder.rb @@ -9,9 +9,9 @@ require 'rubygems/command' class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder - def self.build(extension, directory, dest_path, results) + def self.build(extension, directory, dest_path, results, args=[]) cmd = "#{Gem.ruby} #{File.basename extension}" - cmd << " #{Gem::Command.build_args.join ' '}" unless Gem::Command.build_args.empty? + cmd << " #{args.join ' '}" unless args.empty? run cmd, results diff --git a/lib/rubygems/ext/rake_builder.rb b/lib/rubygems/ext/rake_builder.rb index a1df694366..2ac6edd5c8 100644 --- a/lib/rubygems/ext/rake_builder.rb +++ b/lib/rubygems/ext/rake_builder.rb @@ -9,10 +9,10 @@ require 'rubygems/command' class Gem::Ext::RakeBuilder < Gem::Ext::Builder - def self.build(extension, directory, dest_path, results) + def self.build(extension, directory, dest_path, results, args=[]) if File.basename(extension) =~ /mkrf_conf/i then cmd = "#{Gem.ruby} #{File.basename extension}" - cmd << " #{Gem::Command.build_args.join " "}" unless Gem::Command.build_args.empty? + cmd << " #{args.join " "}" unless args.empty? run cmd, results end diff --git a/lib/rubygems/format.rb b/lib/rubygems/format.rb deleted file mode 100644 index 9644f6ab8e..0000000000 --- a/lib/rubygems/format.rb +++ /dev/null @@ -1,82 +0,0 @@ -#-- -# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. -# All rights reserved. -# See LICENSE.txt for permissions. -#++ - -require 'rubygems/package' - -## -# Gem::Format knows the guts of the RubyGem .gem file format and provides the -# capability to read gem files - -class Gem::Format - - attr_accessor :spec - attr_accessor :file_entries - attr_accessor :gem_path - - ## - # Constructs a Format representing the gem's data which came from +gem_path+ - - def initialize(gem_path) - @gem_path = gem_path - end - - ## - # Reads the gem +file_path+ using +security_policy+ and returns a Format - # representing the data in the gem - - def self.from_file_by_path(file_path, security_policy = nil) - unless File.file?(file_path) - raise Gem::Exception, "Cannot load gem at [#{file_path}] in #{Dir.pwd}" - end - - start = File.read file_path, 20 - - if start.nil? or start.length < 20 then - nil - elsif start.include?("MD5SUM =") # old version gems - require 'rubygems/old_format' - - Gem::OldFormat.from_file_by_path file_path - else - begin - open file_path, Gem.binary_mode do |io| - from_io io, file_path, security_policy - end - rescue Gem::Package::TarInvalidError => e - message = "corrupt gem (#{e.class}: #{e.message})" - raise Gem::Package::FormatError.new(message, file_path) - end - end - end - - ## - # Reads a gem from +io+ at +gem_path+ using +security_policy+ and returns a - # Format representing the data from the gem - - def self.from_io(io, gem_path="(io)", security_policy = nil) - format = new gem_path - - Gem::Package.open io, 'r', security_policy do |pkg| - format.spec = pkg.metadata - format.file_entries = [] - - pkg.each do |entry| - size = entry.header.size - mode = entry.header.mode - - format.file_entries << [{ - "size" => size, "mode" => mode, "path" => entry.full_name, - }, - entry.read - ] - end - end - - format - end - -end - diff --git a/lib/rubygems/gem_openssl.rb b/lib/rubygems/gem_openssl.rb deleted file mode 100644 index 682058f2c1..0000000000 --- a/lib/rubygems/gem_openssl.rb +++ /dev/null @@ -1,90 +0,0 @@ -#-- -# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. -# All rights reserved. -# See LICENSE.txt for permissions. -#++ - -#-- -# Some system might not have OpenSSL installed, therefore the core -# library file openssl might not be available. We localize testing -# for the presence of OpenSSL in this file. -#++ - -module Gem - class << self - ## - # Is SSL (used by the signing commands) available on this - # platform? - - def ssl_available? - @ssl_available - end - - ## - # Is SSL available? - - attr_writer :ssl_available - - ## - # Ensure that SSL is available. Throw an exception if it is not. - - def ensure_ssl_available - unless ssl_available? - raise Gem::Exception, "SSL is not installed on this system" - end - end - end -end - -# :stopdoc: - -begin - require 'openssl' - - # Reference a constant defined in the .rb portion of ssl (just to - # make sure that part is loaded too). - - Gem.ssl_available = !!OpenSSL::Digest::SHA1 - - class OpenSSL::X509::Certificate - # Check the validity of this certificate. - def check_validity(issuer_cert = nil, time = Time.now) - ret = if @not_before && @not_before > time - [false, :expired, "not valid before '#@not_before'"] - elsif @not_after && @not_after < time - [false, :expired, "not valid after '#@not_after'"] - elsif issuer_cert && !verify(issuer_cert.public_key) - [false, :issuer, "#{issuer_cert.subject} is not issuer"] - else - [true, :ok, 'Valid certificate'] - end - - # return hash - { :is_valid => ret[0], :error => ret[1], :desc => ret[2] } - end - end - -rescue LoadError, StandardError - Gem.ssl_available = false -end - -module Gem::SSL - - # We make our own versions of the constants here. This allows us - # to reference the constants, even though some systems might not - # have SSL installed in the Ruby core package. - # - # These constants are only used during load time. At runtime, any - # method that makes a direct reference to SSL software must be - # protected with a Gem.ensure_ssl_available call. - - if Gem.ssl_available? then - PKEY_RSA = OpenSSL::PKey::RSA - DIGEST_SHA1 = OpenSSL::Digest::SHA1 - else - PKEY_RSA = :rsa - DIGEST_SHA1 = :sha1 - end - -end - diff --git a/lib/rubygems/gem_path_searcher.rb b/lib/rubygems/gem_path_searcher.rb deleted file mode 100644 index 814b5fb0e5..0000000000 --- a/lib/rubygems/gem_path_searcher.rb +++ /dev/null @@ -1,172 +0,0 @@ -require "rubygems" -require "rubygems/deprecate" - -## -# GemPathSearcher has the capability to find loadable files inside -# gems. It generates data up front to speed up searches later. - -class Gem::GemPathSearcher - - ## - # Initialise the data we need to make searches later. - - def initialize - # We want a record of all the installed gemspecs, in the order we wish to - # examine them. - # TODO: remove this stupid method - @gemspecs = init_gemspecs - - # Map gem spec to glob of full require_path directories. Preparing this - # information may speed up searches later. - @lib_dirs = {} - - @gemspecs.each do |spec| - @lib_dirs[spec.object_id] = lib_dirs_for spec - end - end - - ## - # Look in all the installed gems until a matching +glob+ is found. - # Return the _gemspec_ of the gem where it was found. If no match - # is found, return nil. - # - # The gems are searched in alphabetical order, and in reverse - # version order. - # - # For example: - # - # find('log4r') # -> (log4r-1.1 spec) - # find('log4r.rb') # -> (log4r-1.1 spec) - # find('rake/rdoctask') # -> (rake-0.4.12 spec) - # find('foobarbaz') # -> nil - # - # Matching paths can have various suffixes ('.rb', '.so', and - # others), which may or may not already be attached to _file_. - # This method doesn't care about the full filename that matches; - # only that there is a match. - - def find(glob) - # HACK violation of encapsulation - @gemspecs.find do |spec| - # TODO: inverted responsibility - matching_file? spec, glob - end - end - - # Looks through the available gemspecs and finds the first - # one that contains +file+ as a requirable file. - - def find_spec_for_file(file) - @gemspecs.find do |spec| - return spec if spec.contains_requirable_file?(file) - end - end - - def find_active(glob) - # HACK violation of encapsulation - @gemspecs.find do |spec| - # TODO: inverted responsibility - spec.loaded? and matching_file? spec, glob - end - end - - ## - # Works like #find, but finds all gemspecs matching +glob+. - - def find_all(glob) - # HACK violation of encapsulation - @gemspecs.select do |spec| - # TODO: inverted responsibility - matching_file? spec, glob - end || [] - end - - def find_in_unresolved(glob) - # HACK violation - specs = Gem.unresolved_deps.values.map { |dep| - Gem.source_index.search dep, true - }.flatten - - specs.select do |spec| - # TODO: inverted responsibility - matching_file? spec, glob - end || [] - end - - def find_in_unresolved_tree glob - # HACK violation - # TODO: inverted responsibility - specs = Gem.unresolved_deps.values.map { |dep| - Gem.source_index.search dep, true - }.flatten - - specs.reverse_each do |spec| - trails = matching_paths(spec, glob) - next if trails.empty? - return trails.map(&:reverse).sort.first.reverse - end - - [] - end - - ## - # Attempts to find a matching path using the require_paths of the given - # +spec+. - - def matching_file?(spec, path) - not matching_files(spec, path).empty? - end - - def matching_paths(spec, path) - trails = [] - - spec.traverse do |from_spec, dep, to_spec, trail| - next unless to_spec.conflicts.empty? - trails << trail unless matching_files(to_spec, path).empty? - end - - trails - end - - ## - # Returns files matching +path+ in +spec+. - #-- - # Some of the intermediate results are cached in @lib_dirs for speed. - - def matching_files(spec, path) - return [] unless @lib_dirs[spec.object_id] # case no paths - glob = File.join @lib_dirs[spec.object_id], "#{path}#{Gem.suffix_pattern}" - Dir[glob].select { |f| File.file? f.untaint } - end - - ## - # Return a list of all installed gemspecs, sorted by alphabetical order and - # in reverse version order. (bar-2, bar-1, foo-2) - - def init_gemspecs - Gem::Specification.sort { |a, b| - names = a.name <=> b.name - next names if names.nonzero? - b.version <=> a.version - } - end - - ## - # Returns library directories glob for a gemspec. For example, - # '/usr/local/lib/ruby/gems/1.8/gems/foobar-1.0/{lib,ext}' - - def lib_dirs_for(spec) - "#{spec.full_gem_path}/{#{spec.require_paths.join(',')}}" if - spec.require_paths - end - - extend Gem::Deprecate - - deprecate :initialize, :none, 2011, 10 - deprecate :find, :none, 2011, 10 - deprecate :find_active, :none, 2011, 10 - deprecate :find_all, :none, 2011, 10 - deprecate :find_in_unresolved, :none, 2011, 10 - deprecate :find_in_unresolved_tree, :none, 2011, 10 - deprecate :find_spec_for_file, :none, 2011, 10 -end diff --git a/lib/rubygems/gem_runner.rb b/lib/rubygems/gem_runner.rb index 6197036f81..8060e15312 100644 --- a/lib/rubygems/gem_runner.rb +++ b/lib/rubygems/gem_runner.rb @@ -4,10 +4,9 @@ # See LICENSE.txt for permissions. #++ -require "rubygems" +require 'rubygems' require 'rubygems/command_manager' require 'rubygems/config_file' -require 'rubygems/doc_manager' ## # Load additional plugins from $LOAD_PATH @@ -29,25 +28,21 @@ class Gem::GemRunner # TODO: nuke these options @command_manager_class = options[:command_manager] || Gem::CommandManager @config_file_class = options[:config_file] || Gem::ConfigFile - @doc_manager_class = options[:doc_manager] || Gem::DocManager end ## # Run the gem command with the following arguments. def run(args) - start_time = Time.now - if args.include?('--') # We need to preserve the original ARGV to use for passing gem options # to source gems. If there is a -- in the line, strip all options after # it...its for the source building process. + # TODO use slice! build_args = args[args.index("--") + 1...args.length] args = args[0...args.index("--")] end - Gem::Command.build_args = build_args if build_args - do_configuration args cmd = @command_manager_class.instance @@ -62,14 +57,7 @@ class Gem::GemRunner Gem::Command.add_specific_extra_args command_name, config_args end - cmd.run Gem.configuration.args - end_time = Time.now - - if Gem.configuration.benchmark then - printf "\nExecution time: %0.2f seconds.\n", end_time - start_time - puts "Press Enter to finish" - STDIN.gets - end + cmd.run Gem.configuration.args, build_args end private @@ -78,7 +66,6 @@ class Gem::GemRunner Gem.configuration = @config_file_class.new(args) Gem.use_paths Gem.configuration[:gemhome], Gem.configuration[:gempath] Gem::Command.extra_args = Gem.configuration[:gem] - @doc_manager_class.configured_args = Gem.configuration[:rdoc] end end diff --git a/lib/rubygems/gemcutter_utilities.rb b/lib/rubygems/gemcutter_utilities.rb index c0e7ee99e9..3042092125 100644 --- a/lib/rubygems/gemcutter_utilities.rb +++ b/lib/rubygems/gemcutter_utilities.rb @@ -1,6 +1,7 @@ require 'rubygems/remote_fetcher' module Gem::GemcutterUtilities + # TODO: move to Gem::Command OptionParser.accept Symbol do |value| value.to_sym end @@ -19,6 +20,8 @@ module Gem::GemcutterUtilities def api_key if options[:key] then verify_api_key options[:key] + elsif Gem.configuration.api_keys.key?(host) + Gem.configuration.api_keys[host] else Gem.configuration.rubygems_api_key end @@ -44,12 +47,24 @@ module Gem::GemcutterUtilities end end - def rubygems_api_request(method, path, host = Gem.host, &block) + attr_writer :host + def host + configured_host = Gem.host unless + Gem.configuration.disable_default_gem_server + + @host ||= ENV['RUBYGEMS_HOST'] || configured_host + end + + def rubygems_api_request(method, path, host = nil, &block) require 'net/http' - host = ENV['RUBYGEMS_HOST'] if ENV['RUBYGEMS_HOST'] - uri = URI.parse "#{host}/#{path}" - say "Pushing gem to #{host}..." + self.host = host if host + unless self.host + alert_error "You must specify a gem server" + terminate_interaction 1 # TODO: question this + end + + uri = URI.parse "#{self.host}/#{path}" request_method = Net::HTTP.const_get method.to_s.capitalize @@ -66,7 +81,7 @@ module Gem::GemcutterUtilities end else say resp.body - terminate_interaction 1 + terminate_interaction 1 # TODO: question this end end @@ -74,8 +89,8 @@ module Gem::GemcutterUtilities if Gem.configuration.api_keys.key? key then Gem.configuration.api_keys[key] else - alert_error "No such API key. You can add it with gem keys --add #{key}" - terminate_interaction 1 + alert_error "No such API key. Please add it to your configuration (done automatically on initial `gem push`)." + terminate_interaction 1 # TODO: question this end end diff --git a/lib/rubygems/indexer.rb b/lib/rubygems/indexer.rb index e87e5a3632..77282f27ef 100644 --- a/lib/rubygems/indexer.rb +++ b/lib/rubygems/indexer.rb @@ -1,5 +1,5 @@ require 'rubygems' -require 'rubygems/format' +require 'rubygems/package' require 'time' begin @@ -15,11 +15,6 @@ class Gem::Indexer include Gem::UserInteraction - ## - # Build indexes for RubyGems older than 1.2.0 when true - - attr_accessor :build_legacy - ## # Build indexes for RubyGems 1.2.0 and newer when true @@ -63,15 +58,10 @@ class Gem::Indexer "\n\tgem install builder" end - options = { :build_legacy => true, :build_modern => true }.merge options + options = { :build_modern => true }.merge options - @build_legacy = options[:build_legacy] @build_modern = options[:build_modern] - @rss_title = options[:rss_title] - @rss_host = options[:rss_host] - @rss_gems_host = options[:rss_gems_host] - @dest_directory = directory @directory = File.join(Dir.tmpdir, "gem_generate_index_#{$$}") @@ -99,8 +89,6 @@ class Gem::Indexer @dest_prerelease_specs_index = File.join(@dest_directory, "prerelease_specs.#{Gem.marshal_version}") - @rss_index = File.join @directory, 'index.rss' - @files = [] end @@ -109,6 +97,8 @@ class Gem::Indexer # searching, downloading and related activities and do not need deployment # specific information (e.g. list of files). So we abbreviate the spec, # making it much smaller for quicker downloads. + #-- + # TODO move to Gem::Specification def abbreviate(spec) spec.files = [] @@ -123,37 +113,15 @@ class Gem::Indexer # Build various indicies def build_indicies - # Marshal gemspecs are used by both modern and legacy RubyGems - Gem::Specification.dirs = [] Gem::Specification.add_specs(*map_gems_to_specs(gem_file_list)) build_marshal_gemspecs - build_legacy_indicies if @build_legacy build_modern_indicies if @build_modern - build_rss compress_indicies end - ## - # Builds indicies for RubyGems older than 1.2.x - - def build_legacy_indicies - index = collect_specs - - say "Generating Marshal master index" - - Gem.time 'Generated Marshal master index' do - open @marshal_index, 'wb' do |io| - io.write index.dump - end - end - - @files << @marshal_index - @files << "#{@marshal_index}.Z" - end - ## # Builds Marshal quick index gemspecs. @@ -238,104 +206,6 @@ class Gem::Indexer "#{@prerelease_specs_index}.gz"] end - ## - # Builds an RSS feed for past two days gem releases according to the gem's - # date. - - def build_rss - if @rss_host.nil? or @rss_gems_host.nil? then - if Gem.configuration.really_verbose then - alert_warning "no --rss-host or --rss-gems-host, RSS generation disabled" - end - return - end - - require 'cgi' - require 'rubygems/text' - - extend Gem::Text - - Gem.time 'Generated rss' do - open @rss_index, 'wb' do |io| - rss_host = CGI.escapeHTML @rss_host - rss_title = CGI.escapeHTML(@rss_title || 'gems') - - io.puts <<-HEADER - - - - #{rss_title} - http://#{rss_host} - Recently released gems from http://#{rss_host} - RubyGems v#{Gem::VERSION} - http://cyber.law.harvard.edu/rss/rss.html - HEADER - - today = Gem::Specification::TODAY - yesterday = today - 86400 - - index = Gem::Specification.select do |spec| - spec_date = spec.date - # TODO: remove this and make YAML based specs properly normalized - spec_date = Time.parse(spec_date.to_s) if Date === spec_date - - spec_date >= yesterday && spec_date <= today - end - - index.sort_by { |spec| [-spec.date.to_i, spec] }.each do |spec| - file_name = File.basename spec.cache_file - gem_path = CGI.escapeHTML "http://#{@rss_gems_host}/gems/#{file_name}" - size = File.stat(spec.loaded_from).size # rescue next - - description = spec.description || spec.summary || '' - authors = Array spec.authors - emails = Array spec.email - authors = emails.zip(authors).map do |email, author| - email += " (#{author})" if author and not author.empty? - end.join ', ' - - description = description.split(/\n\n+/).map do |chunk| - format_text chunk, 78 - end - - description = description.join "\n\n" - - item = '' - - item << <<-ITEM - - #{CGI.escapeHTML spec.full_name} - -<pre>#{CGI.escapeHTML description.chomp}</pre> - - #{CGI.escapeHTML authors} - #{CGI.escapeHTML spec.full_name} - - #{spec.date.rfc2822} - ITEM - - item << <<-ITEM if spec.homepage - #{CGI.escapeHTML spec.homepage} - ITEM - - item << <<-ITEM - - ITEM - - io.puts item - end - - io.puts <<-FOOTER - - - FOOTER - end - end - - @files << @rss_index - end - def map_gems_to_specs gems gems.map { |gemfile| if File.size(gemfile) == 0 then @@ -344,7 +214,7 @@ class Gem::Indexer end begin - spec = Gem::Format.from_file_by_path(gemfile).spec + spec = Gem::Package.new(gemfile).spec spec.loaded_from = gemfile # HACK: fuck this shit - borks all tests that use pl1 @@ -373,21 +243,6 @@ class Gem::Indexer }.compact end - ## - # Collect specifications from .gem files from the gem directory. - - def collect_specs(gems = gem_file_list) - Gem::Deprecate.skip_during do - index = Gem::SourceIndex.new - - map_gems_to_specs(gems).each do |spec| - index.add_spec spec, spec.original_name - end - - index - end - end - ## # Compresses indicies on disk #-- @@ -397,11 +252,6 @@ class Gem::Indexer say "Compressing indicies" Gem.time 'Compressed indicies' do - if @build_legacy then - compress @marshal_index, 'Z' - paranoid @marshal_index, 'Z' - end - if @build_modern then gzip @specs_index gzip @latest_specs_index @@ -559,12 +409,9 @@ class Gem::Indexer end ## - # Perform an in-place update of the repository from newly added gems. Only - # works for modern indicies, and sets #build_legacy to false when run. + # Perform an in-place update of the repository from newly added gems. def update_index - @build_legacy = false - make_temp_directories specs_mtime = File.stat(@dest_specs_index).mtime @@ -584,6 +431,9 @@ class Gem::Indexer specs = map_gems_to_specs updated_gems prerelease, released = specs.partition { |s| s.version.prerelease? } + Gem::Specification.dirs = [] + Gem::Specification.add_specs(*specs) + files = build_marshal_gemspecs Gem.time 'Updated indexes' do diff --git a/lib/rubygems/install_message.rb b/lib/rubygems/install_message.rb new file mode 100644 index 0000000000..c1979c1549 --- /dev/null +++ b/lib/rubygems/install_message.rb @@ -0,0 +1,12 @@ +require 'rubygems' +require 'rubygems/user_interaction' + +## +# A default post-install hook that displays "Successfully installed +# some_gem-1.0" + +Gem.post_install do |installer| + ui = Gem::DefaultUserInteraction.ui + ui.say "Successfully installed #{installer.spec.full_name}" +end + diff --git a/lib/rubygems/install_update_options.rb b/lib/rubygems/install_update_options.rb index 3ee6432b4c..ffa8f910df 100644 --- a/lib/rubygems/install_update_options.rb +++ b/lib/rubygems/install_update_options.rb @@ -22,6 +22,7 @@ module Gem::InstallUpdateOptions # Add the install/update options to the option parser. def add_install_update_options + # TODO: use @parser.accept OptionParser.accept Gem::Security::Policy do |value| require 'rubygems/security' @@ -39,21 +40,49 @@ module Gem::InstallUpdateOptions end add_option(:"Install/Update", '-n', '--bindir DIR', - 'Directory where binary files are', - 'located') do |value, options| + 'Directory where binary files are', + 'located') do |value, options| options[:bin_dir] = File.expand_path(value) end - add_option(:"Install/Update", '-d', '--[no-]rdoc', - 'Generate RDoc documentation for the gem on', - 'install') do |value, options| - options[:generate_rdoc] = value + add_option(:"Install/Update", '--[no-]document [TYPES]', Array, + 'Generate documentation for installed gems', + 'List the documentation types you wish to', + 'generate. For example: rdoc,ri') do |value, options| + options[:document] = case value + when nil then %w[ri] + when false then [] + else value + end end - add_option(:"Install/Update", '--[no-]ri', - 'Generate RI documentation for the gem on', - 'install') do |value, options| - options[:generate_ri] = value + add_option(:"Install/Update", '-N', '--no-document', + 'Disable documentation generation') do |value, options| + options[:document] = [] + end + + add_option(:Deprecated, '--[no-]rdoc', + 'Generate RDoc for installed gems', + 'Use --document instead') do |value, options| + if value then + options[:document] << 'rdoc' + else + options[:document].delete 'rdoc' + end + + options[:document].uniq! + end + + add_option(:Deprecated, '--[no-]ri', + 'Generate ri data for installed gems.', + 'Use --document instead') do |value, options| + if value then + options[:document] << 'ri' + else + options[:document].delete 'ri' + end + + options[:document].uniq! end add_option(:"Install/Update", '-E', '--[no-]env-shebang', @@ -85,12 +114,6 @@ module Gem::InstallUpdateOptions options[:ignore_dependencies] = value end - add_option(:"Install/Update", '-y', '--include-dependencies', - 'Unconditionally install the required', - 'dependent gems') do |value, options| - options[:include_dependencies] = value - end - add_option(:"Install/Update", '--[no-]format-executable', 'Make installed executable names match ruby.', 'If ruby is ruby18, foo_exec will be', @@ -105,15 +128,30 @@ module Gem::InstallUpdateOptions end add_option(:"Install/Update", "--development", - "Install any additional development", + "Install additional development", "dependencies") do |value, options| options[:development] = true + options[:dev_shallow] = true + end + + add_option(:"Install/Update", "--development-all", + "Install development dependencies for all", + "gems (including dev deps themselves)") do |value, options| + options[:development] = true + options[:dev_shallow] = false end add_option(:"Install/Update", "--conservative", "Don't attempt to upgrade gems already", "meeting version requirement") do |value, options| options[:conservative] = true + options[:minimal_deps] = true + end + + add_option(:"Install/Update", "--minimal-deps", + "Don't upgrade any dependencies that already", + "meet version requirements") do |value, options| + options[:minimal_deps] = true end end @@ -121,7 +159,7 @@ module Gem::InstallUpdateOptions # Default options for the gem install command. def install_update_defaults_str - '--rdoc --no-force --wrappers' + '--document=rdoc,ri --wrappers' end end diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 514316f099..2b7c821727 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -4,15 +4,13 @@ # See LICENSE.txt for permissions. #++ -require 'rubygems/format' require 'rubygems/exceptions' +require 'rubygems/package' require 'rubygems/ext' -require 'rubygems/require_paths_builder' require 'rubygems/user_interaction' ## -# The installer class processes RubyGem .gem files and installs the files -# contained in the .gem into the Gem.path. +# The installer installs the files contained in the .gem into the Gem.home. # # Gem::Installer does the work of putting files in all the right places on the # filesystem including unpacking the gem into its gem dir, installing the @@ -39,8 +37,7 @@ class Gem::Installer include Gem::UserInteraction - include Gem::RequirePathsBuilder if Gem::QUICKLOADER_SUCKAGE - + # DOC: Missing docs or :nodoc:. attr_reader :gem ## @@ -67,6 +64,7 @@ class Gem::Installer attr_accessor :path_warning + # DOC: Missing docs or :nodoc:. attr_writer :exec_format # Defaults to use Ruby's program prefix and suffix. @@ -80,24 +78,36 @@ class Gem::Installer # Constructs an Installer instance that will install the gem located at # +gem+. +options+ is a Hash with the following keys: # + # :bin_dir:: Where to put a bin wrapper if needed. + # :development:: Whether or not development dependencies should be installed. # :env_shebang:: Use /usr/bin/env in bin wrappers. # :force:: Overrides all version checks and security policy checks, except # for a signed-gems-only policy. - # :ignore_dependencies:: Don't raise if a dependency is missing. - # :install_dir:: The directory to install the gem into. # :format_executable:: Format the executable the same as the ruby executable. # If your ruby is ruby18, foo_exec will be installed as # foo_exec18. + # :ignore_dependencies:: Don't raise if a dependency is missing. + # :install_dir:: The directory to install the gem into. # :security_policy:: Use the specified security policy. See Gem::Security + # :user_install:: Indicate that the gem should be unpacked into the users + # personal gem directory. + # :only_install_dir:: Only validate dependencies against what is in the + # install_dir # :wrappers:: Install wrappers if true, symlinks if false. + # :build_args:: An Array of arguments to pass to the extension builder + # process. If not set, then Gem::Command.build_args is used def initialize(gem, options={}) require 'fileutils' @gem = gem @options = options + @package = Gem::Package.new @gem + process_options + @package.security_policy = @security_policy + if options[:user_install] and not options[:unpack] then @gem_home = Gem.user_dir check_that_user_bin_dir_is_in_path @@ -105,28 +115,79 @@ class Gem::Installer end ## - # Lazy accessor for the spec's gem directory. + # Checks if +filename+ exists in +@bin_dir+. + # + # If +@force+ is set +filename+ is overwritten. + # + # If +filename+ exists and is a RubyGems wrapper for different gem the user + # is consulted. + # + # If +filename+ exists and +@bin_dir+ is Gem.default_bindir (/usr/local) the + # user is consulted. + # + # Otherwise +filename+ is overwritten. - def gem_dir - @gem_dir ||= spec.gem_dir.dup.untaint + def check_executable_overwrite filename # :nodoc: + return if @force + + generated_bin = File.join @bin_dir, filename + + return unless File.exist? generated_bin + + ruby_executable = false + existing = nil + + open generated_bin, 'rb' do |io| + next unless io.gets =~ /^#!/ # shebang + io.gets # blankline + + # TODO detect a specially formatted comment instead of trying + # to run a regexp against ruby code. + next unless io.gets =~ /This file was generated by RubyGems/ + + ruby_executable = true + existing = io.read.slice(/^gem (['"])(.*?)(\1),/, 2) + end + + return if spec.name == existing + + # somebody has written to RubyGems' directory, overwrite, too bad + return if Gem.default_bindir != @bin_dir and not ruby_executable + + question = "#{spec.name}'s executable \"#{filename}\" conflicts with " + + if ruby_executable then + question << existing + + return if ask_yes_no "#{question}\nOverwrite the executable?", false + + conflict = "installed executable from #{existing}" + else + question << generated_bin + + return if ask_yes_no "#{question}\nOverwrite the executable?", false + + conflict = generated_bin + end + + raise Gem::InstallError, + "\"#{filename}\" from #{spec.name} conflicts with #{conflict}" end ## - # Lazy accessor for the installer's Gem::Format instance. + # Lazy accessor for the spec's gem directory. - def format - begin - @format ||= Gem::Format.from_file_by_path gem, @security_policy - rescue Gem::Package::FormatError - raise Gem::InstallError, "invalid gem format for #{gem}" - end + def gem_dir + @gem_dir ||= File.join(gem_home, "gems", spec.full_name) end ## # Lazy accessor for the installer's spec. def spec - @spec ||= format.spec + @spec ||= @package.spec + rescue Gem::Package::Error => e + raise Gem::InstallError, "invalid gem: #{e.message}" end ## @@ -141,11 +202,7 @@ class Gem::Installer # specifications/.gemspec #=> the Gem::Specification def install - current_home = Gem.dir - current_path = Gem.paths.path - verify_gem_home(options[:unpack]) - Gem.use_paths gem_home, current_path # HACK: shouldn't need Gem.paths.path # If we're forcing the install then disable security unless the security # policy says that we only install signed gems. @@ -158,65 +215,96 @@ class Gem::Installer ensure_dependencies_met unless @ignore_dependencies end - Gem.pre_install_hooks.each do |hook| - result = hook.call self - - if result == false then - location = " at #{$1}" if hook.inspect =~ /@(.*:\d+)/ - - message = "pre-install hook#{location} failed for #{spec.full_name}" - raise Gem::InstallError, message - end - end + run_pre_install_hooks Gem.ensure_gem_subdirectories gem_home # Completely remove any previous gem files - FileUtils.rm_rf(gem_dir) if File.exist? gem_dir + FileUtils.rm_rf(gem_dir) FileUtils.mkdir_p gem_dir extract_files build_extensions - Gem.post_build_hooks.each do |hook| - result = hook.call self - - if result == false then - FileUtils.rm_rf gem_dir + run_post_build_hooks - location = " at #{$1}" if hook.inspect =~ /@(.*:\d+)/ + generate_bin + write_spec - message = "post-build hook#{location} failed for #{spec.full_name}" - raise Gem::InstallError, message + unless @build_args.empty? + File.open spec.build_info_file, "w" do |f| + @build_args.each { |a| f.puts a } end end - generate_bin - write_spec - - write_require_paths_file_if_needed if Gem::QUICKLOADER_SUCKAGE + # TODO should be always cache the file? Other classes have options + # to controls if caching is done. + cache_file = File.join(gem_home, "cache", "#{spec.full_name}.gem") - cache_file = spec.cache_file FileUtils.cp gem, cache_file unless File.exist? cache_file say spec.post_install_message unless spec.post_install_message.nil? - spec.loaded_from = spec.spec_file + spec.loaded_from = spec_file Gem::Specification.add_spec spec unless Gem::Specification.include? spec + run_post_install_hooks + + spec + + # TODO This rescue is in the wrong place. What is raising this exception? + # move this rescue to arround the code that actually might raise it. + rescue Zlib::GzipFile::Error + raise Gem::InstallError, "gzip error installing #{gem}" + end + + def run_pre_install_hooks # :nodoc: + Gem.pre_install_hooks.each do |hook| + if hook.call(self) == false then + location = " at #{$1}" if hook.inspect =~ /@(.*:\d+)/ + + message = "pre-install hook#{location} failed for #{spec.full_name}" + raise Gem::InstallError, message + end + end + end + + def run_post_build_hooks # :nodoc: + Gem.post_build_hooks.each do |hook| + if hook.call(self) == false then + FileUtils.rm_rf gem_dir + + location = " at #{$1}" if hook.inspect =~ /@(.*:\d+)/ + + message = "post-build hook#{location} failed for #{spec.full_name}" + raise Gem::InstallError, message + end + end + end + + def run_post_install_hooks # :nodoc: Gem.post_install_hooks.each do |hook| hook.call self end + end - return spec - rescue Zlib::GzipFile::Error - raise Gem::InstallError, "gzip error installing #{gem}" - ensure - # conditional since we might be here because we're erroring out early. - if current_path - Gem.use_paths current_home, current_path + ## + # + # Return an Array of Specifications contained within the gem_home + # we'll be installing into. + + def installed_specs + @specs ||= begin + specs = [] + + Dir[File.join(gem_home, "specifications", "*.gemspec")].each do |path| + spec = Gem::Specification.load path.untaint + specs << spec if spec + end + + specs end end @@ -235,9 +323,11 @@ class Gem::Installer end ## - # True if the gems in the source_index satisfy +dependency+. + # True if the gems in the system satisfy +dependency+. def installation_satisfies_dependency?(dependency) + return true if installed_specs.detect { |s| dependency.matches_spec? s } + return false if @only_install_dir not dependency.matching_specs.empty? end @@ -246,18 +336,23 @@ class Gem::Installer def unpack(directory) @gem_dir = directory - @format = Gem::Format.from_file_by_path gem, @security_policy extract_files end + ## + # The location of of the spec file that is installed. + # + + def spec_file + File.join gem_home, "specifications", "#{spec.full_name}.gemspec" + end + ## # Writes the .gemspec specification (in Ruby) to the gem home's # specifications directory. def write_spec - file_name = spec.spec_file.untaint - - File.open(file_name, "w") do |file| + File.open(spec_file, "w") do |file| file.puts spec.to_ruby_for_cache end end @@ -277,34 +372,34 @@ class Gem::Installer end end + # DOC: Missing docs or :nodoc:. def generate_bin return if spec.executables.nil? or spec.executables.empty? - # If the user has asked for the gem to be installed in a directory that is - # the system gem directory, then use the system bin directory, else create - # (or use) a new bin dir under the gem_home. - bindir = @bin_dir || Gem.bindir(gem_home) - - Dir.mkdir bindir unless File.exist? bindir - raise Gem::FilePermissionError.new(bindir) unless File.writable? bindir + Dir.mkdir @bin_dir unless File.exist? @bin_dir + raise Gem::FilePermissionError.new(@bin_dir) unless File.writable? @bin_dir spec.executables.each do |filename| filename.untaint - bin_path = File.expand_path File.join(gem_dir, spec.bindir, filename) + bin_path = File.join gem_dir, spec.bindir, filename - unless File.exist? bin_path - warn "Hey?!?! Where did #{bin_path} go??" + unless File.exist? bin_path then + # TODO change this to a more useful warning + warn "#{bin_path} maybe `gem pristine #{spec.name}` will fix it?" next end mode = File.stat(bin_path).mode | 0111 FileUtils.chmod mode, bin_path + check_executable_overwrite filename + if @wrappers then - generate_bin_script filename, bindir + generate_bin_script filename, @bin_dir else - generate_bin_symlink filename, bindir + generate_bin_symlink filename, @bin_dir end + end end @@ -358,10 +453,21 @@ class Gem::Installer ## # Generates a #! line for +bin_file_name+'s wrapper copying arguments if # necessary. + # + # If the :custom_shebang config is set, then it is used as a template + # for how to create the shebang used for to run a gem's executables. + # + # The template supports 4 expansions: + # + # $env the path to the unix env utility + # $ruby the path to the currently running ruby interpreter + # $exec the path to the gem's executable + # $name the name of the gem the executable is for + # def shebang(bin_file_name) ruby_name = Gem::ConfigMap[:ruby_install_name] if @env_shebang - path = spec.bin_file bin_file_name + path = File.join gem_dir, spec.bindir, bin_file_name first_line = File.open(path, "rb") {|file| file.gets} if /\A#!/ =~ first_line then @@ -371,7 +477,25 @@ class Gem::Installer shebang.strip! # Avoid nasty ^M issues. end - if not ruby_name then + if which = Gem.configuration[:custom_shebang] + # replace bin_file_name with "ruby" to avoid endless loops + which = which.gsub(/ #{bin_file_name}$/," #{Gem::ConfigMap[:ruby_install_name]}") + + which = which.gsub(/\$(\w+)/) do + case $1 + when "env" + @env_path ||= ENV_PATHS.find {|env_path| File.executable? env_path } + when "ruby" + "#{Gem.ruby}#{opts}" + when "exec" + bin_file_name + when "name" + spec.name + end + end + + "#!#{which}" + elsif not ruby_name then "#!#{Gem.ruby}#{opts}" elsif opts then "#!/bin/sh\n'exec' #{ruby_name.dump} '-x' \"$0\" \"$@\"\n#{shebang}" @@ -382,6 +506,7 @@ class Gem::Installer end end + # DOC: Missing docs or :nodoc:. def ensure_required_ruby_version_met if rrv = spec.required_ruby_version then unless rrv.satisfied_by? Gem.ruby_version then @@ -390,9 +515,10 @@ class Gem::Installer end end + # DOC: Missing docs or :nodoc:. def ensure_required_rubygems_version_met if rrgv = spec.required_rubygems_version then - unless rrgv.satisfied_by? Gem::Version.new(Gem::VERSION) then + unless rrgv.satisfied_by? Gem.rubygems_version then raise Gem::InstallError, "#{spec.name} requires RubyGems version #{rrgv}. " + "Try 'gem update --system' to update RubyGems itself." @@ -400,6 +526,7 @@ class Gem::Installer end end + # DOC: Missing docs or :nodoc:. def ensure_dependencies_met deps = spec.runtime_dependencies deps |= spec.development_dependencies if @development @@ -409,13 +536,14 @@ class Gem::Installer end end + # DOC: Missing docs or :nodoc:. def process_options @options = { :bin_dir => nil, :env_shebang => false, - :exec_format => false, :force => false, :install_dir => Gem.dir, + :only_install_dir => false }.merge options @env_shebang = options[:env_shebang] @@ -425,13 +553,18 @@ class Gem::Installer @format_executable = options[:format_executable] @security_policy = options[:security_policy] @wrappers = options[:wrappers] - @bin_dir = options[:bin_dir] + @only_install_dir = options[:only_install_dir] + + # If the user has asked for the gem to be installed in a directory that is + # the system gem directory, then use the system bin directory, else create + # (or use) a new bin dir under the gem_home. + @bin_dir = options[:bin_dir] || Gem.bindir(gem_home) @development = options[:development] - raise "NOTE: Installer option :source_index is dead" if - options[:source_index] + @build_args = options[:build_args] || Gem::Command.build_args end + # DOC: Missing docs or :nodoc:. def check_that_user_bin_dir_is_in_path user_bin_dir = @bin_dir || Gem.bindir(gem_home) user_bin_dir.gsub!(File::SEPARATOR, File::ALT_SEPARATOR) if File::ALT_SEPARATOR @@ -449,6 +582,7 @@ class Gem::Installer end end + # DOC: Missing docs or :nodoc:. def verify_gem_home(unpack = false) FileUtils.mkdir_p gem_home raise Gem::FilePermissionError, gem_home unless @@ -499,7 +633,6 @@ GOTO :EOF :WinNT @"#{ruby}" "%~dpn0" %* TEXT - end ## @@ -508,7 +641,14 @@ TEXT def build_extensions return if spec.extensions.empty? - say "Building native extensions. This could take a while..." + + if @build_args.empty? + say "Building native extensions. This could take a while..." + else + say "Building native extensions with: '#{@build_args.join(' ')}'" + say "This could take a while..." + end + dest_path = File.join gem_dir, spec.require_paths.first ran_rake = false # only run rake once @@ -516,6 +656,9 @@ TEXT break if ran_rake results = [] + extension ||= "" + extension_dir = File.join gem_dir, File.dirname(extension) + builder = case extension when /extconf/ then Gem::Ext::ExtConfBuilder @@ -525,43 +668,41 @@ TEXT ran_rake = true Gem::Ext::RakeBuilder else - results = ["No builder for extension '#{extension}'"] - nil + message = "No builder for extension '#{extension}'" + extension_build_error extension_dir, message end - - extension_dir = begin - File.join gem_dir, File.dirname(extension) - rescue TypeError # extension == nil - gem_dir - end - - begin Dir.chdir extension_dir do - results = builder.build(extension, gem_dir, dest_path, results) + results = builder.build(extension, gem_dir, dest_path, + results, @build_args) say results.join("\n") if Gem.configuration.really_verbose end rescue - results = results.join "\n" + extension_build_error(extension_dir, results.join("\n")) + end + end + end - gem_make_out = File.join extension_dir, 'gem_make.out' + ## + # Logs the build +output+ in +build_dir+, then raises ExtensionBuildError. + + def extension_build_error(build_dir, output) + gem_make_out = File.join build_dir, 'gem_make.out' - open gem_make_out, 'wb' do |io| io.puts results end + open gem_make_out, 'wb' do |io| io.puts output end - message = <<-EOF + message = <<-EOF ERROR: Failed to build gem native extension. - #{results} + #{output} Gem files will remain installed in #{gem_dir} for inspection. Results logged to #{gem_make_out} EOF - raise ExtensionBuildError, message - end - end + raise ExtensionBuildError, message end ## @@ -570,36 +711,7 @@ EOF # Ensures that files can't be installed outside the gem directory. def extract_files - raise ArgumentError, "format required to extract from" if @format.nil? - - @format.file_entries.each do |entry, file_data| - path = entry['path'].untaint - - if path.start_with? "/" then # for extra sanity - raise Gem::InstallError, "attempt to install file into #{entry['path']}" - end - - path = File.expand_path File.join(gem_dir, path) - - unless path.start_with? gem_dir then - msg = "attempt to install file into %p under %s" % - [entry['path'], gem_dir] - raise Gem::InstallError, msg - end - - FileUtils.rm_rf(path) if File.exist? path - - dir = File.dirname path - FileUtils.mkdir_p dir unless File.exist? dir - - File.open(path, "wb") do |out| - out.write file_data - end - - FileUtils.chmod entry['mode'], path - - say path if Gem.configuration.really_verbose - end + @package.extract_files gem_dir end ## diff --git a/lib/rubygems/installer_test_case.rb b/lib/rubygems/installer_test_case.rb index 96a5156995..62a468a8a2 100644 --- a/lib/rubygems/installer_test_case.rb +++ b/lib/rubygems/installer_test_case.rb @@ -6,11 +6,26 @@ class Gem::Installer ## # Available through requiring rubygems/installer_test_case + attr_writer :bin_dir + + ## + # Available through requiring rubygems/installer_test_case + + attr_writer :build_args + + ## + # Available through requiring rubygems/installer_test_case + attr_writer :gem_dir ## # Available through requiring rubygems/installer_test_case + attr_writer :force + + ## + # Available through requiring rubygems/installer_test_case + attr_writer :format ## @@ -54,47 +69,68 @@ end class Gem::InstallerTestCase < Gem::TestCase + ## + # Creates the following instance variables: + # + # @spec:: + # a spec named 'a', intended for regular installs + # @user_spec:: + # a spec named 'b', intended for user installs + + # @gem:: + # the path to a built gem from @spec + # @user_spec:: + # the path to a built gem from @user_spec + # + # @installer:: + # a Gem::Installer for the @spec that installs into @gemhome + # @user_installer:: + # a Gem::Installer for the @user_spec that installs into Gem.user_dir + def setup super - @installer_tmp = File.join @tempdir, 'installer' - FileUtils.mkdir_p @installer_tmp + @spec = quick_gem 'a' do |spec| + util_make_exec spec + end - Gem.use_paths @installer_tmp - Gem.ensure_gem_subdirectories @installer_tmp + @user_spec = quick_gem 'b' do |spec| + util_make_exec spec + end - @spec = quick_gem 'a' - util_make_exec @spec util_build_gem @spec - @gem = @spec.cache_file - - @user_spec = quick_gem 'b' - util_make_exec @user_spec util_build_gem @user_spec - @user_gem = @user_spec.cache_file - Gem.use_paths @gemhome + @gem = @spec.cache_file + @user_gem = @user_spec.cache_file @installer = util_installer @spec, @gemhome @user_installer = util_installer @user_spec, Gem.user_dir, :user - - Gem.use_paths @gemhome end - def util_gem_bindir spec = @spec + def util_gem_bindir spec = @spec # :nodoc: # TODO: deprecate spec.bin_dir end - def util_gem_dir spec = @spec + def util_gem_dir spec = @spec # :nodoc: # TODO: deprecate spec.gem_dir end + ## + # The path where installed executables live + def util_inst_bindir File.join @gemhome, "bin" end + ## + # Adds an executable named "executable" to +spec+ with the given +shebang+. + # + # The executable is also written to the bin dir in @tmpdir and the installed + # gem directory for +spec+. + def util_make_exec(spec = @spec, shebang = "#!/usr/bin/ruby") spec.executables = %w[executable] spec.files << 'bin/executable' @@ -110,6 +146,14 @@ class Gem::InstallerTestCase < Gem::TestCase end end + ## + # Builds the @spec gem and returns an installer for it. The built gem + # includes: + # + # bin/executable + # lib/code.rb + # ext/a/mkrf_conf.rb + def util_setup_gem(ui = @ui) # HACK fix use_ui to make this automatic @spec.files << File.join('lib', 'code.rb') @spec.extensions << File.join('ext', 'a', 'mkrf_conf.rb') @@ -129,16 +173,24 @@ class Gem::InstallerTestCase < Gem::TestCase end use_ui ui do - FileUtils.rm @gem + FileUtils.rm_f @gem - @gem = Gem::Builder.new(@spec).build + @gem = Gem::Package.build @spec end end @installer = Gem::Installer.new @gem end + ## + # Creates an installer for +spec+ that will install into +gem_home+. If + # +user+ is true a user-install will be performed. + def util_installer(spec, gem_home, user=false) - Gem::Installer.new spec.cache_file, :user_install => user + Gem::Installer.new(spec.cache_file, + :install_dir => gem_home, + :user_install => user) end + end + diff --git a/lib/rubygems/mock_gem_ui.rb b/lib/rubygems/mock_gem_ui.rb index 13f0bf564b..76a9389676 100644 --- a/lib/rubygems/mock_gem_ui.rb +++ b/lib/rubygems/mock_gem_ui.rb @@ -6,6 +6,17 @@ require 'rubygems/user_interaction' # retrieval during tests. class Gem::MockGemUi < Gem::StreamUI + ## + # Raised when you haven't provided enough input to your MockGemUi + + class InputEOFError < RuntimeError + + def initialize question + super "Out of input for MockGemUi on #{question.inspect}" + end + + end + class TermError < RuntimeError attr_reader :exit_code @@ -44,6 +55,12 @@ class Gem::MockGemUi < Gem::StreamUI @terminated = false end + def ask question + raise InputEOFError, question if @ins.eof? + + super + end + def input @ins.string end diff --git a/lib/rubygems/name_tuple.rb b/lib/rubygems/name_tuple.rb new file mode 100644 index 0000000000..d16fad26f1 --- /dev/null +++ b/lib/rubygems/name_tuple.rb @@ -0,0 +1,110 @@ +## +# +# Represents a gem of name +name+ at +version+ of +platform+. These +# wrap the data returned from the indexes. + +require 'rubygems/platform' + +class Gem::NameTuple + def initialize(name, version, platform="ruby") + @name = name + @version = version + + unless platform.kind_of? Gem::Platform + platform = "ruby" if !platform or platform.empty? + end + + @platform = platform + end + + attr_reader :name, :version, :platform + + ## + # Turn an array of [name, version, platform] into an array of + # NameTuple objects. + + def self.from_list list + list.map { |t| new(*t) } + end + + ## + # Turn an array of NameTuple objects back into an array of + # [name, version, platform] tuples. + + def self.to_basic list + list.map { |t| t.to_a } + end + + ## + # A null NameTuple, ie name=nil, version=0 + + def self.null + new nil, Gem::Version.new(0), nil + end + + ## + # Indicate if this NameTuple matches the current platform. + + def match_platform? + Gem::Platform.match @platform + end + + ## + # Indicate if this NameTuple is for a prerelease version. + def prerelease? + @version.prerelease? + end + + ## + # Return the name that the gemspec file would be + + def spec_name + case @platform + when nil, 'ruby', '' + "#{@name}-#{@version}.gemspec" + else + "#{@name}-#{@version}-#{@platform}.gemspec" + end + end + + ## + # Convert back to the [name, version, platform] tuple + + def to_a + [@name, @version, @platform] + end + + def to_s + "#" + end + + def <=> other + to_a <=> other.to_a + end + + include Comparable + + ## + # Compare with +other+. Supports another NameTuple or an Array + # in the [name, version, platform] format. + + def == other + case other + when self.class + @name == other.name and + @version == other.version and + @platform == other.platform + when Array + to_a == other + else + false + end + end + + alias_method :eql?, :== + + def hash + to_a.hash + end + +end diff --git a/lib/rubygems/old_format.rb b/lib/rubygems/old_format.rb deleted file mode 100644 index a44fd533a5..0000000000 --- a/lib/rubygems/old_format.rb +++ /dev/null @@ -1,153 +0,0 @@ -#-- -# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. -# All rights reserved. -# See LICENSE.txt for permissions. -#++ - -require 'rubygems' - -## -# The format class knows the guts of the RubyGem .gem file format and provides -# the capability to read gem files - -class Gem::OldFormat - - attr_accessor :spec, :file_entries, :gem_path - - ## - # Constructs an instance of a Format object, representing the gem's data - # structure. - # - # gem:: [String] The file name of the gem - - def initialize(gem_path) - require 'fileutils' - require 'zlib' - Gem.load_yaml - - @gem_path = gem_path - end - - ## - # Reads the named gem file and returns a Format object, representing the - # data from the gem file - # - # file_path:: [String] Path to the gem file - - def self.from_file_by_path(file_path) - unless File.exist?(file_path) - raise Gem::Exception, "Cannot load gem file [#{file_path}]" - end - - File.open(file_path, 'rb') do |file| - from_io(file, file_path) - end - end - - ## - # Reads a gem from an io stream and returns a Format object, representing - # the data from the gem file - # - # io:: [IO] Stream from which to read the gem - - def self.from_io(io, gem_path="(io)") - format = self.new(gem_path) - skip_ruby(io) - format.spec = read_spec(io) - format.file_entries = [] - read_files_from_gem(io) do |entry, file_data| - format.file_entries << [entry, file_data] - end - format - end - - private - - ## - # Skips the Ruby self-install header. After calling this method, the - # IO index will be set after the Ruby code. - # - # file:: [IO] The IO to process (skip the Ruby code) - - def self.skip_ruby(file) - end_seen = false - loop { - line = file.gets - if(line == nil || line.chomp == "__END__") then - end_seen = true - break - end - } - - if end_seen == false then - raise Gem::Exception.new("Failed to find end of ruby script while reading gem") - end - end - - ## - # Reads the specification YAML from the supplied IO and constructs - # a Gem::Specification from it. After calling this method, the - # IO index will be set after the specification header. - # - # file:: [IO] The IO to process - - def self.read_spec(file) - yaml = '' - - read_until_dashes file do |line| - yaml << line - end - - Gem::Specification.from_yaml yaml - rescue YAML::Error => e - raise Gem::Exception, "Failed to parse gem specification out of gem file" - rescue ArgumentError => e - raise Gem::Exception, "Failed to parse gem specification out of gem file" - end - - ## - # Reads lines from the supplied IO until a end-of-yaml (---) is - # reached - # - # file:: [IO] The IO to process - # block:: [String] The read line - - def self.read_until_dashes(file) - while((line = file.gets) && line.chomp.strip != "---") do - yield line - end - end - - ## - # Reads the embedded file data from a gem file, yielding an entry - # containing metadata about the file and the file contents themselves - # for each file that's archived in the gem. - # NOTE: Many of these methods should be extracted into some kind of - # Gem file read/writer - # - # gem_file:: [IO] The IO to process - - def self.read_files_from_gem(gem_file) - errstr = "Error reading files from gem" - header_yaml = '' - begin - self.read_until_dashes(gem_file) do |line| - header_yaml << line - end - header = YAML.load(header_yaml) - raise Gem::Exception, errstr unless header - - header.each do |entry| - file_data = '' - self.read_until_dashes(gem_file) do |line| - file_data << line - end - yield [entry, Zlib::Inflate.inflate(file_data.strip.unpack("m")[0])] - end - rescue Zlib::DataError - raise Gem::Exception, errstr - end - end - -end - diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index 2b50c588ee..51df43be93 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.rb @@ -3,16 +3,54 @@ # Copyright (C) 2004 Mauricio Julio Fernández Pradier # See LICENSE.txt for additional licensing information. #++ +# +# Example using a Gem::Package +# +# Builds a .gem file given a Gem::Specification. A .gem file is a tarball +# which contains a data.tar.gz and metadata.gz, and possibly signatures. +# +# require 'rubygems' +# require 'rubygems/package' +# +# spec = Gem::Specification.new do |s| +# s.summary = "Ruby based make-like utility." +# s.name = 'rake' +# s.version = PKG_VERSION +# s.requirements << 'none' +# s.files = PKG_FILES +# s.description = <<-EOF +# Rake is a Make-like program implemented in Ruby. Tasks +# and dependencies are specified in standard Ruby syntax. +# EOF +# end +# +# Gem::Package.build spec +# +# Reads a .gem file. +# +# require 'rubygems' +# require 'rubygems/package' +# +# the_gem = Gem::Package.new(path_to_dot_gem) +# the_gem.contents # get the files in the gem +# the_gem.extract_files destination_directory # extract the gem into a directory +# the_gem.spec # get the spec out of the gem +# the_gem.verify # check the gem is OK (contains valid gem specification, contains a not corrupt contents archive) +# +# #files are the files in the .gem tar file, not the ruby files in the gem +# #extract_files and #contents automatically call #verify +require 'rubygems/security' require 'rubygems/specification' +require 'rubygems/user_interaction' +require 'zlib' -module Gem::Package +class Gem::Package + + include Gem::UserInteraction + + class Error < Gem::Exception; end - class Error < StandardError; end - class NonSeekableIO < Error; end - class ClosedIO < Error; end - class BadCheckSum < Error; end - class TooLongFileName < Error; end class FormatError < Error attr_reader :path @@ -26,57 +64,489 @@ module Gem::Package end + class PathError < Error + def initialize destination, destination_dir + super "installing into parent path %s of %s is not allowed" % + [destination, destination_dir] + end + end + + class NonSeekableIO < Error; end + + class TooLongFileName < Error; end + ## # Raised when a tar file is corrupt class TarInvalidError < Error; end - # FIX: zenspider said: does it really take an IO? - # passed to a method called open?!? that seems stupid. - def self.open(io, mode = "r", signer = nil, &block) - tar_type = case mode - when 'r' then TarInput - when 'w' then TarOutput - else - raise "Unknown Package open mode" - end - - tar_type.open(io, signer, &block) - end - - def self.pack(src, destname, signer = nil) - TarOutput.open(destname, signer) do |outp| - dir_class.chdir(src) do - outp.metadata = (file_class.read("RPA/metadata") rescue nil) - find_class.find('.') do |entry| - case - when file_class.file?(entry) - entry.sub!(%r{\./}, "") - next if entry =~ /\ARPA\// - stat = File.stat(entry) - outp.add_file_simple(entry, stat.mode, stat.size) do |os| - file_class.open(entry, "rb") do |f| - os.write(f.read(4096)) until f.eof? - end - end - when file_class.dir?(entry) - entry.sub!(%r{\./}, "") - next if entry == "RPA" - outp.mkdir(entry, file_class.stat(entry).mode) - else - raise "Don't know how to pack this yet!" + attr_accessor :build_time # :nodoc: + + ## + # Checksums for the contents of the package + + attr_reader :checksums + + ## + # The files in this package. This is not the contents of the gem, just the + # files in the top-level container. + + attr_reader :files + + ## + # The security policy used for verifying the contents of this package. + + attr_accessor :security_policy + + ## + # Sets the Gem::Specification to use to build this package. + + attr_writer :spec + + def self.build spec, skip_validation=false + gem_file = spec.file_name + + package = new gem_file + package.spec = spec + package.build skip_validation + + gem_file + end + + ## + # Creates a new Gem::Package for the file at +gem+. + # + # If +gem+ is an existing file in the old format a Gem::Package::Old will be + # returned. + + def self.new gem + return super unless Gem::Package == self + return super unless File.exist? gem + + start = File.read gem, 20 + + return super unless start + return super unless start.include? 'MD5SUM =' + + Gem::Package::Old.new gem + end + + ## + # Creates a new package that will read or write to the file +gem+. + + def initialize gem # :notnew: + @gem = gem + + @build_time = Time.now + @checksums = {} + @contents = nil + @digests = Hash.new { |h, algorithm| h[algorithm] = {} } + @files = nil + @security_policy = nil + @signatures = {} + @signer = nil + @spec = nil + end + + ## + # Adds a checksum for each entry in the gem to checksums.yaml.gz. + + def add_checksums tar + Gem.load_yaml + + checksums_by_algorithm = Hash.new { |h, algorithm| h[algorithm] = {} } + + @checksums.each do |name, digests| + digests.each do |algorithm, digest| + checksums_by_algorithm[algorithm][name] = digest.hexdigest + end + end + + tar.add_file_signed 'checksums.yaml.gz', 0444, @signer do |io| + gzip_to io do |gz_io| + YAML.dump checksums_by_algorithm, gz_io + end + end + end + + ## + # Adds the files listed in the packages's Gem::Specification to data.tar.gz + # and adds this file to the +tar+. + + def add_contents tar # :nodoc: + digests = tar.add_file_signed 'data.tar.gz', 0444, @signer do |io| + gzip_to io do |gz_io| + Gem::Package::TarWriter.new gz_io do |data_tar| + add_files data_tar + end + end + end + + @checksums['data.tar.gz'] = digests + end + + ## + # Adds files included the package's Gem::Specification to the +tar+ file + + def add_files tar # :nodoc: + @spec.files.each do |file| + stat = File.stat file + + tar.add_file_simple file, stat.mode, stat.size do |dst_io| + open file, 'rb' do |src_io| + dst_io.write src_io.read 16384 until src_io.eof? + end + end + end + end + + ## + # Adds the package's Gem::Specification to the +tar+ file + + def add_metadata tar # :nodoc: + digests = tar.add_file_signed 'metadata.gz', 0444, @signer do |io| + gzip_to io do |gz_io| + gz_io.write @spec.to_yaml + end + end + + @checksums['metadata.gz'] = digests + end + + ## + # Builds this package based on the specification set by #spec= + + def build skip_validation = false + require 'rubygems/security' + + @spec.validate unless skip_validation + @spec.mark_version + + setup_signer + + open @gem, 'wb' do |gem_io| + Gem::Package::TarWriter.new gem_io do |gem| + add_metadata gem + add_contents gem + add_checksums gem + end + end + + say <<-EOM + Successfully built RubyGem + Name: #{@spec.name} + Version: #{@spec.version} + File: #{File.basename @spec.cache_file} +EOM + ensure + @signer = nil + end + + ## + # A list of file names contained in this gem + + def contents + return @contents if @contents + + verify unless @spec + + @contents = [] + + open @gem, 'rb' do |io| + gem_tar = Gem::Package::TarReader.new io + + gem_tar.each do |entry| + next unless entry.full_name == 'data.tar.gz' + + open_tar_gz entry do |pkg_tar| + pkg_tar.each do |contents_entry| + @contents << contents_entry.full_name end end + + return @contents + end + end + end + + ## + # Creates a digest of the TarEntry +entry+ from the digest algorithm set by + # the security policy. + + def digest entry # :nodoc: + return unless @checksums + + @checksums.each_key do |algorithm| + digester = OpenSSL::Digest.new algorithm + + digester << entry.read(16384) until entry.eof? + + entry.rewind + + @digests[algorithm][entry.full_name] = digester + end + + @digests + end + + ## + # Extracts the files in this package into +destination_dir+ + + def extract_files destination_dir + verify unless @spec + + FileUtils.mkdir_p destination_dir + + open @gem, 'rb' do |io| + reader = Gem::Package::TarReader.new io + + reader.each do |entry| + next unless entry.full_name == 'data.tar.gz' + + extract_tar_gz entry, destination_dir + + return # ignore further entries + end + end + end + + ## + # Extracts all the files in the gzipped tar archive +io+ into + # +destination_dir+. + # + # If an entry in the archive contains a relative path above + # +destination_dir+ or an absolute path is encountered an exception is + # raised. + + def extract_tar_gz io, destination_dir # :nodoc: + open_tar_gz io do |tar| + tar.each do |entry| + destination = install_location entry.full_name, destination_dir + + FileUtils.rm_rf destination + + FileUtils.mkdir_p File.dirname destination + + open destination, 'wb', entry.header.mode do |out| + out.write entry.read + out.fsync rescue nil # for filesystems without fsync(2) + end + + say destination if Gem.configuration.really_verbose + end + end + end + + ## + # Gzips content written to +gz_io+ to +io+. + #-- + # Also sets the gzip modification time to the package build time to ease + # testing. + + def gzip_to io # :yields: gz_io + gz_io = Zlib::GzipWriter.new io, Zlib::BEST_COMPRESSION + gz_io.mtime = @build_time + + yield gz_io + ensure + gz_io.close + end + + ## + # Returns the full path for installing +filename+. + # + # If +filename+ is not inside +destination_dir+ an exception is raised. + + def install_location filename, destination_dir # :nodoc: + raise Gem::Package::PathError.new(filename, destination_dir) if + filename.start_with? '/' + + destination = File.join destination_dir, filename + destination = File.expand_path destination + + raise Gem::Package::PathError.new(destination, destination_dir) unless + destination.start_with? destination_dir + + destination.untaint + destination + end + + ## + # Loads a Gem::Specification from the TarEntry +entry+ + + def load_spec entry # :nodoc: + case entry.full_name + when 'metadata' then + @spec = Gem::Specification.from_yaml entry.read + when 'metadata.gz' then + args = [entry] + args << { :external_encoding => Encoding::UTF_8 } if + Object.const_defined? :Encoding + + Zlib::GzipReader.wrap(*args) do |gzio| + @spec = Gem::Specification.from_yaml gzio.read + end + end + end + + ## + # Opens +io+ as a gzipped tar archive + + def open_tar_gz io # :nodoc: + Zlib::GzipReader.wrap io do |gzio| + tar = Gem::Package::TarReader.new gzio + + yield tar + end + end + + ## + # Reads and loads checksums.yaml.gz from the tar file +gem+ + + def read_checksums gem + Gem.load_yaml + + @checksums = gem.seek 'checksums.yaml.gz' do |entry| + Zlib::GzipReader.wrap entry do |gz_io| + YAML.load gz_io.read + end + end + end + + ## + # Prepares the gem for signing and checksum generation. If a signing + # certificate and key are not present only checksum generation is set up. + + def setup_signer + if @spec.signing_key then + @signer = Gem::Security::Signer.new @spec.signing_key, @spec.cert_chain + @spec.signing_key = nil + @spec.cert_chain = @signer.cert_chain.map { |cert| cert.to_s } + else + @signer = Gem::Security::Signer.new nil, nil + @spec.cert_chain = @signer.cert_chain.map { |cert| cert.to_pem } if + @signer.cert_chain + end + end + + ## + # The spec for this gem. + # + # If this is a package for a built gem the spec is loaded from the + # gem and returned. If this is a package for a gem being built the provided + # spec is returned. + + def spec + verify unless @spec + + @spec + end + + ## + # Verifies that this gem: + # + # * Contains a valid gem specification + # * Contains a contents archive + # * The contents archive is not corrupt + # + # After verification the gem specification from the gem is available from + # #spec + + def verify + @files = [] + @spec = nil + + open @gem, 'rb' do |io| + Gem::Package::TarReader.new io do |reader| + read_checksums reader + + verify_files reader + end + end + + verify_checksums @digests, @checksums + + @security_policy.verify_signatures @spec, @digests, @signatures if + @security_policy + + true + rescue Errno::ENOENT => e + raise Gem::Package::FormatError.new e.message + rescue Gem::Package::TarInvalidError => e + raise Gem::Package::FormatError.new e.message, @gem + end + + ## + # Verifies the +checksums+ against the +digests+. This check is not + # cryptographically secure. Missing checksums are ignored. + + def verify_checksums digests, checksums # :nodoc: + return unless checksums + + checksums.sort.each do |algorithm, gem_digests| + gem_digests.sort.each do |file_name, gem_hexdigest| + computed_digest = digests[algorithm][file_name] + + unless computed_digest.hexdigest == gem_hexdigest then + raise Gem::Package::FormatError.new \ + "#{algorithm} checksum mismatch for #{file_name}", @gem + end end end end + ## + # Verifies the files of the +gem+ + + def verify_files gem + gem.each do |entry| + file_name = entry.full_name + @files << file_name + + case file_name + when /\.sig$/ then + @signatures[$`] = entry.read if @security_policy + next + when 'checksums.yaml.gz' then + next # already handled + else + digest entry + end + + case file_name + when /^metadata(.gz)?$/ then + load_spec entry + when 'data.tar.gz' then + verify_gz entry + end + end + + unless @spec then + raise Gem::Package::FormatError.new 'package metadata is missing', @gem + end + + unless @files.include? 'data.tar.gz' then + raise Gem::Package::FormatError.new \ + 'package content (data.tar.gz) is missing', @gem + end + end + + ## + # Verifies that +entry+ is a valid gzipped file. + + def verify_gz entry # :nodoc: + Zlib::GzipReader.wrap entry do |gzio| + gzio.read 16384 until gzio.eof? # gzip checksum verification + end + rescue Zlib::GzipFile::Error => e + raise Gem::Package::FormatError.new(e.message, entry.full_name) + end + end -require 'rubygems/package/f_sync_dir' +require 'rubygems/package/digest_io' +require 'rubygems/package/old' require 'rubygems/package/tar_header' -require 'rubygems/package/tar_input' -require 'rubygems/package/tar_output' require 'rubygems/package/tar_reader' require 'rubygems/package/tar_reader/entry' require 'rubygems/package/tar_writer' diff --git a/lib/rubygems/package/digest_io.rb b/lib/rubygems/package/digest_io.rb new file mode 100644 index 0000000000..f8bde0f557 --- /dev/null +++ b/lib/rubygems/package/digest_io.rb @@ -0,0 +1,64 @@ +## +# IO wrapper that creates digests of contents written to the IO it wraps. + +class Gem::Package::DigestIO + + ## + # Collected digests for wrapped writes. + # + # { + # 'SHA1' => #, + # 'SHA512' => #, + # } + + attr_reader :digests + + ## + # Wraps +io+ and updates digest for each of the digest algorithms in + # the +digests+ Hash. Returns the digests hash. Example: + # + # io = StringIO.new + # digests = { + # 'SHA1' => OpenSSL::Digest.new('SHA1'), + # 'SHA512' => OpenSSL::Digest.new('SHA512'), + # } + # + # Gem::Package::DigestIO.wrap io, digests do |digest_io| + # digest_io.write "hello" + # end + # + # digests['SHA1'].hexdigest #=> "aaf4c61d[...]" + # digests['SHA512'].hexdigest #=> "9b71d224[...]" + + def self.wrap io, digests + digest_io = new io, digests + + yield digest_io + + return digests + end + + ## + # Creates a new DigestIO instance. Using ::wrap is recommended, see the + # ::wrap documentation for documentation of +io+ and +digests+. + + def initialize io, digests + @io = io + @digests = digests + end + + ## + # Writes +data+ to the underlying IO and updates the digests + + def write data + result = @io.write data + + @digests.each do |_, digest| + digest << data + end + + result + end + +end + diff --git a/lib/rubygems/package/f_sync_dir.rb b/lib/rubygems/package/f_sync_dir.rb deleted file mode 100644 index f7eb7f3ce3..0000000000 --- a/lib/rubygems/package/f_sync_dir.rb +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -#-- -# Copyright (C) 2004 Mauricio Julio Fernández Pradier -# See LICENSE.txt for additional licensing information. -#++ - -module Gem::Package::FSyncDir - - private - - ## - # make sure this hits the disc - - def fsync_dir(dirname) - dir = open dirname, 'r' - dir.fsync - rescue # ignore IOError if it's an unpatched (old) Ruby - ensure - dir.close if dir rescue nil - end - -end - diff --git a/lib/rubygems/package/old.rb b/lib/rubygems/package/old.rb new file mode 100644 index 0000000000..552a5f3591 --- /dev/null +++ b/lib/rubygems/package/old.rb @@ -0,0 +1,147 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +## +# The format class knows the guts of the ancient .gem file format and provides +# the capability to read such ancient gems. +# +# Please pretend this doesn't exist. + +class Gem::Package::Old < Gem::Package + + undef_method :spec= + + ## + # Creates a new old-format package reader for +gem+. Old-format packages + # cannot be written. + + def initialize gem + require 'fileutils' + require 'zlib' + Gem.load_yaml + + @gem = gem + @contents = nil + @spec = nil + end + + ## + # A list of file names contained in this gem + + def contents + return @contents if @contents + + open @gem, 'rb' do |io| + read_until_dashes io # spec + header = file_list io + + @contents = header.map { |file| file['path'] } + end + end + + ## + # Extracts the files in this package into +destination_dir+ + + def extract_files destination_dir + errstr = "Error reading files from gem" + + open @gem, 'rb' do |io| + read_until_dashes io # spec + header = file_list io + raise Gem::Exception, errstr unless header + + header.each do |entry| + full_name = entry['path'] + + destination = install_location full_name, destination_dir + + file_data = '' + + read_until_dashes io do |line| + file_data << line + end + + file_data = file_data.strip.unpack("m")[0] + file_data = Zlib::Inflate.inflate file_data + + raise Gem::Package::FormatError, "#{full_name} in #{@gem} is corrupt" if + file_data.length != entry['size'].to_i + + FileUtils.rm_rf destination + + FileUtils.mkdir_p File.dirname destination + + open destination, 'wb', entry['mode'] do |out| + out.write file_data + end + + say destination if Gem.configuration.really_verbose + end + end + rescue Zlib::DataError + raise Gem::Exception, errstr + end + + ## + # Reads the file list section from the old-format gem +io+ + + def file_list io # :nodoc: + header = '' + + read_until_dashes io do |line| + header << line + end + + YAML.load header + end + + ## + # Reads lines until a "---" separator is found + + def read_until_dashes io # :nodoc: + while (line = io.gets) && line.chomp.strip != "---" do + yield line if block_given? + end + end + + ## + # Skips the Ruby self-install header in +io+. + + def skip_ruby io # :nodoc: + loop do + line = io.gets + + return if line.chomp == '__END__' + break unless line + end + + raise Gem::Exception, "Failed to find end of ruby script while reading gem" + end + + ## + # The specification for this gem + + def spec + return @spec if @spec + + yaml = '' + + open @gem, 'rb' do |io| + skip_ruby io + read_until_dashes io do |line| + yaml << line + end + end + + @spec = Gem::Specification.from_yaml yaml + rescue YAML::SyntaxError => e + raise Gem::Exception, "Failed to parse gem specification out of gem file" + rescue ArgumentError => e + raise Gem::Exception, "Failed to parse gem specification out of gem file" + end + +end + diff --git a/lib/rubygems/package/tar_header.rb b/lib/rubygems/package/tar_header.rb index 4f923b9b5e..28da1db0b5 100644 --- a/lib/rubygems/package/tar_header.rb +++ b/lib/rubygems/package/tar_header.rb @@ -102,61 +102,24 @@ class Gem::Package::TarHeader fields = header.unpack UNPACK_FORMAT - name = fields.shift - mode = fields.shift.oct - uid = fields.shift.oct - gid = fields.shift.oct - size = fields.shift.oct - mtime = fields.shift.oct - checksum = fields.shift.oct - typeflag = fields.shift - linkname = fields.shift - magic = fields.shift - version = fields.shift.oct - uname = fields.shift - gname = fields.shift - devmajor = fields.shift.oct - devminor = fields.shift.oct - prefix = fields.shift - - new :name => name, - :mode => mode, - :uid => uid, - :gid => gid, - :size => size, - :mtime => mtime, - :checksum => checksum, - :typeflag => typeflag, - :linkname => linkname, - :magic => magic, - :version => version, - :uname => uname, - :gname => gname, - :devmajor => devmajor, - :devminor => devminor, - :prefix => prefix, - - :empty => empty - - # HACK unfactor for Rubinius - #new :name => fields.shift, - # :mode => fields.shift.oct, - # :uid => fields.shift.oct, - # :gid => fields.shift.oct, - # :size => fields.shift.oct, - # :mtime => fields.shift.oct, - # :checksum => fields.shift.oct, - # :typeflag => fields.shift, - # :linkname => fields.shift, - # :magic => fields.shift, - # :version => fields.shift.oct, - # :uname => fields.shift, - # :gname => fields.shift, - # :devmajor => fields.shift.oct, - # :devminor => fields.shift.oct, - # :prefix => fields.shift, - - # :empty => empty + new :name => fields.shift, + :mode => fields.shift.oct, + :uid => fields.shift.oct, + :gid => fields.shift.oct, + :size => fields.shift.oct, + :mtime => fields.shift.oct, + :checksum => fields.shift.oct, + :typeflag => fields.shift, + :linkname => fields.shift, + :magic => fields.shift, + :version => fields.shift.oct, + :uname => fields.shift, + :gname => fields.shift, + :devmajor => fields.shift.oct, + :devminor => fields.shift.oct, + :prefix => fields.shift, + + :empty => empty end ## diff --git a/lib/rubygems/package/tar_input.rb b/lib/rubygems/package/tar_input.rb deleted file mode 100644 index 77b4d698da..0000000000 --- a/lib/rubygems/package/tar_input.rb +++ /dev/null @@ -1,235 +0,0 @@ -# -*- coding: iso-8859-1 -*- -#++ -# Copyright (C) 2004 Mauricio Julio Fernández Pradier -# See LICENSE.txt for additional licensing information. -#-- - -require 'zlib' -Gem.load_yaml - -class Gem::Package::TarInput - - include Gem::Package::FSyncDir - include Enumerable - - attr_reader :metadata - - private_class_method :new - - def self.open(io, security_policy = nil, &block) - is = new io, security_policy - - yield is - ensure - is.close if is - end - - def initialize(io, security_policy = nil) - @io = io - @tarreader = Gem::Package::TarReader.new @io - has_meta = false - - data_sig, meta_sig, data_dgst, meta_dgst = nil, nil, nil, nil - dgst_algo = security_policy ? Gem::Security::OPT[:dgst_algo] : nil - - @tarreader.each do |entry| - case entry.full_name - when "metadata" - @metadata = load_gemspec entry.read - has_meta = true - when "metadata.gz" - begin - # if we have a security_policy, then pre-read the metadata file - # and calculate it's digest - sio = nil - if security_policy - Gem.ensure_ssl_available - sio = StringIO.new(entry.read) - meta_dgst = dgst_algo.digest(sio.string) - sio.rewind - end - - # Ruby 1.8 doesn't have encoding and YAML is UTF-8 - args = [sio || entry] - args << { :external_encoding => Encoding::UTF_8 } if - Object.const_defined?(:Encoding) - - gzis = Zlib::GzipReader.new(*args) - - # YAML wants an instance of IO - @metadata = load_gemspec(gzis) - has_meta = true - ensure - gzis.close unless gzis.nil? - end - when 'metadata.gz.sig' - meta_sig = entry.read - when 'data.tar.gz.sig' - data_sig = entry.read - when 'data.tar.gz' - if security_policy - Gem.ensure_ssl_available - data_dgst = dgst_algo.digest(entry.read) - end - end - end - - if security_policy then - Gem.ensure_ssl_available - - # map trust policy from string to actual class (or a serialized YAML - # file, if that exists) - if String === security_policy then - if Gem::Security::Policies.key? security_policy then - # load one of the pre-defined security policies - security_policy = Gem::Security::Policies[security_policy] - elsif File.exist? security_policy then - # FIXME: this doesn't work yet - security_policy = YAML.load File.read(security_policy) - else - raise Gem::Exception, "Unknown trust policy '#{security_policy}'" - end - end - - if data_sig && data_dgst && meta_sig && meta_dgst then - # the user has a trust policy, and we have a signed gem - # file, so use the trust policy to verify the gem signature - - begin - security_policy.verify_gem(data_sig, data_dgst, @metadata.cert_chain) - rescue Exception => e - raise "Couldn't verify data signature: #{e}" - end - - begin - security_policy.verify_gem(meta_sig, meta_dgst, @metadata.cert_chain) - rescue Exception => e - raise "Couldn't verify metadata signature: #{e}" - end - elsif security_policy.only_signed - raise Gem::Exception, "Unsigned gem" - else - # FIXME: should display warning here (trust policy, but - # either unsigned or badly signed gem file) - end - end - - @tarreader.rewind - - unless has_meta then - path = io.path if io.respond_to? :path - error = Gem::Package::FormatError.new 'no metadata found', path - raise error - end - end - - def close - @io.close - @tarreader.close - end - - def each(&block) - @tarreader.each do |entry| - next unless entry.full_name == "data.tar.gz" - is = zipped_stream entry - - begin - Gem::Package::TarReader.new is do |inner| - inner.each(&block) - end - ensure - is.close if is - end - end - - @tarreader.rewind - end - - def extract_entry(destdir, entry, expected_md5sum = nil) - if entry.directory? then - dest = File.join destdir, entry.full_name - - if File.directory? dest then - FileUtils.chmod entry.header.mode, dest, :verbose => false - else - FileUtils.mkdir_p dest, :mode => entry.header.mode, :verbose => false - end - - fsync_dir dest - fsync_dir File.join(dest, "..") - - return - end - - # it's a file - md5 = Digest::MD5.new if expected_md5sum - destdir = File.join destdir, File.dirname(entry.full_name) - FileUtils.mkdir_p destdir, :mode => 0755, :verbose => false - destfile = File.join destdir, File.basename(entry.full_name) - FileUtils.chmod 0600, destfile, :verbose => false rescue nil # Errno::ENOENT - - open destfile, "wb", entry.header.mode do |os| - loop do - data = entry.read 4096 - break unless data - # HACK shouldn't we check the MD5 before writing to disk? - md5 << data if expected_md5sum - os.write(data) - end - - os.fsync - end - - FileUtils.chmod entry.header.mode, destfile, :verbose => false - fsync_dir File.dirname(destfile) - fsync_dir File.join(File.dirname(destfile), "..") - - if expected_md5sum && expected_md5sum != md5.hexdigest then - raise Gem::Package::BadCheckSum - end - end - - # Attempt to YAML-load a gemspec from the given _io_ parameter. Return - # nil if it fails. - def load_gemspec(io) - Gem::Specification.from_yaml io - rescue Gem::Exception - nil - end - - ## - # Return an IO stream for the zipped entry. - # - # NOTE: Originally this method used two approaches, Return a GZipReader - # directly, or read the GZipReader into a string and return a StringIO on - # the string. The string IO approach was used for versions of ZLib before - # 1.2.1 to avoid buffer errors on windows machines. Then we found that - # errors happened with 1.2.1 as well, so we changed the condition. Then - # we discovered errors occurred with versions as late as 1.2.3. At this - # point (after some benchmarking to show we weren't seriously crippling - # the unpacking speed) we threw our hands in the air and declared that - # this method would use the String IO approach on all platforms at all - # times. And that's the way it is. - # - # Revisited. Here's the beginning of the long story. - # http://osdir.com/ml/lang.ruby.gems.devel/2007-06/msg00045.html - # - # StringIO wraping has never worked as a workaround by definition. Skipping - # initial 10 bytes and passing -MAX_WBITS to Zlib::Inflate luckily works as - # gzip reader, but it only works if the GZip header is 10 bytes long (see - # below) and it does not check inflated stream consistency (CRC value in the - # Gzip trailer.) - # - # RubyGems generated Gzip Header: 10 bytes - # magic(2) + method(1) + flag(1) + mtime(4) + exflag(1) + os(1) + - # orig_name(0) + comment(0) - # - # Ideally, it must return a GZipReader without meaningless buffering. We - # have lots of CRuby committers around so let's fix windows build when we - # received an error. - def zipped_stream(entry) - Zlib::GzipReader.new entry - end - -end - diff --git a/lib/rubygems/package/tar_output.rb b/lib/rubygems/package/tar_output.rb deleted file mode 100644 index fdc8f4fb7c..0000000000 --- a/lib/rubygems/package/tar_output.rb +++ /dev/null @@ -1,146 +0,0 @@ -# -*- coding: utf-8 -*- -#-- -# Copyright (C) 2004 Mauricio Julio Fernández Pradier -# See LICENSE.txt for additional licensing information. -#++ - -## -# TarOutput is a wrapper to TarWriter that builds gem-format tar file. -# -# Gem-format tar files contain the following files: -# [data.tar.gz] A gzipped tar file containing the files that compose the gem -# which will be extracted into the gem/ dir on installation. -# [metadata.gz] A YAML format Gem::Specification. -# [data.tar.gz.sig] A signature for the gem's data.tar.gz. -# [metadata.gz.sig] A signature for the gem's metadata.gz. -# -# See TarOutput::open for usage details. - -class Gem::Package::TarOutput - - ## - # Creates a new TarOutput which will yield a TarWriter object for the - # data.tar.gz portion of a gem-format tar file. - # - # See #initialize for details on +io+ and +signer+. - # - # See #add_gem_contents for details on adding metadata to the tar file. - - def self.open(io, signer = nil, &block) # :yield: data_tar_writer - tar_outputter = new io, signer - tar_outputter.add_gem_contents(&block) - tar_outputter.add_metadata - tar_outputter.add_signatures - - ensure - tar_outputter.close - end - - ## - # Creates a new TarOutput that will write a gem-format tar file to +io+. If - # +signer+ is given, the data.tar.gz and metadata.gz will be signed and - # the signatures will be added to the tar file. - - def initialize(io, signer) - @io = io - @signer = signer - - @tar_writer = Gem::Package::TarWriter.new @io - - @metadata = nil - - @data_signature = nil - @meta_signature = nil - end - - ## - # Yields a TarWriter for the data.tar.gz inside a gem-format tar file. - # The yielded TarWriter has been extended with a #metadata= method for - # attaching a YAML format Gem::Specification which will be written by - # add_metadata. - - def add_gem_contents - @tar_writer.add_file "data.tar.gz", 0644 do |inner| - sio = @signer ? StringIO.new : nil - Zlib::GzipWriter.wrap(sio || inner) do |os| - - Gem::Package::TarWriter.new os do |data_tar_writer| - # :stopdoc: - def data_tar_writer.metadata() @metadata end - def data_tar_writer.metadata=(metadata) @metadata = metadata end - # :startdoc: - - yield data_tar_writer - - @metadata = data_tar_writer.metadata - end - end - - # if we have a signing key, then sign the data - # digest and return the signature - if @signer then - require 'rubygems/security' - digest = Gem::Security::OPT[:dgst_algo].digest sio.string - @data_signature = @signer.sign digest - inner.write sio.string - end - end - - self - end - - ## - # Adds metadata.gz to the gem-format tar file which was saved from a - # previous #add_gem_contents call. - - def add_metadata - return if @metadata.nil? - - @tar_writer.add_file "metadata.gz", 0644 do |io| - begin - sio = @signer ? StringIO.new : nil - gzos = Zlib::GzipWriter.new(sio || io) - gzos.write @metadata - ensure - gzos.flush - gzos.finish - - # if we have a signing key, then sign the metadata digest and return - # the signature - if @signer then - require 'rubygems/security' - digest = Gem::Security::OPT[:dgst_algo].digest sio.string - @meta_signature = @signer.sign digest - io.write sio.string - end - end - end - end - - ## - # Adds data.tar.gz.sig and metadata.gz.sig to the gem-format tar files if - # a Gem::Security::Signer was sent to initialize. - - def add_signatures - if @data_signature then - @tar_writer.add_file 'data.tar.gz.sig', 0644 do |io| - io.write @data_signature - end - end - - if @meta_signature then - @tar_writer.add_file 'metadata.gz.sig', 0644 do |io| - io.write @meta_signature - end - end - end - - ## - # Closes the TarOutput. - - def close - @tar_writer.close - end - -end - diff --git a/lib/rubygems/package/tar_reader.rb b/lib/rubygems/package/tar_reader.rb index e6a71d386c..e257fdd846 100644 --- a/lib/rubygems/package/tar_reader.rb +++ b/lib/rubygems/package/tar_reader.rb @@ -9,7 +9,7 @@ class Gem::Package::TarReader - include Gem::Package + include Enumerable ## # Raised if the tar IO is not seekable @@ -52,9 +52,9 @@ class Gem::Package::TarReader # Iterates over files in the tarball yielding each entry def each - loop do - return if @io.eof? + return enum_for __method__ unless block_given? + until @io.eof? do header = Gem::Package::TarHeader.from @io return if header.empty? @@ -100,6 +100,23 @@ class Gem::Package::TarReader end end + ## + # Seeks through the tar file until it finds the +entry+ with +name+ and + # yields it. Rewinds the tar file to the beginning when the block + # terminates. + + def seek name # :yields: entry + found = find do |entry| + entry.full_name == name + end + + return unless found + + return yield found + ensure + rewind + end + end require 'rubygems/package/tar_reader/entry' diff --git a/lib/rubygems/package/tar_writer.rb b/lib/rubygems/package/tar_writer.rb index a73b5e5cab..f2c11e3544 100644 --- a/lib/rubygems/package/tar_writer.rb +++ b/lib/rubygems/package/tar_writer.rb @@ -40,12 +40,12 @@ class Gem::Package::TarWriter # number of bytes will be more than #limit def write(data) - if data.size + @written > @limit + if data.bytesize + @written > @limit raise FileOverflow, "You tried to feed more data than fits in the file." end @io.write data - @written += data.size - data.size + @written += data.bytesize + data.bytesize end end @@ -129,6 +129,62 @@ class Gem::Package::TarWriter self end + ## + # Adds +name+ with permissions +mode+ to the tar, yielding +io+ for writing + # the file. The +digest_algorithm+ is written to a read-only +name+.sum + # file following the given file contents containing the digest name and + # hexdigest separated by a tab. + # + # The created digest object is returned. + + def add_file_digest name, mode, digest_algorithms # :yields: io + digests = digest_algorithms.map do |digest_algorithm| + digest = digest_algorithm.new + [digest.name, digest] + end + + digests = Hash[*digests.flatten] + + add_file name, mode do |io| + Gem::Package::DigestIO.wrap io, digests do |digest_io| + yield digest_io + end + end + + digests + end + + ## + # Adds +name+ with permissions +mode+ to the tar, yielding +io+ for writing + # the file. The +signer+ is used to add a digest file using its + # digest_algorithm per add_file_digest and a cryptographic signature in + # +name+.sig. If the signer has no key only the checksum file is added. + # + # Returns the digest. + + def add_file_signed name, mode, signer + digest_algorithms = [ + signer.digest_algorithm, + OpenSSL::Digest::SHA512, + ].uniq + + digests = add_file_digest name, mode, digest_algorithms do |io| + yield io + end + + signature_digest = digests.values.find do |digest| + digest.name == signer.digest_name + end + + signature = signer.sign signature_digest.digest + + add_file_simple "#{name}.sig", 0444, signature.length do |io| + io.write signature + end if signature + + digests + end + ## # Add file +name+ with permissions +mode+ +size+ bytes long. Yields an IO # to write the file to. @@ -211,9 +267,9 @@ class Gem::Package::TarWriter # Splits +name+ into a name and prefix that can fit in the TarHeader def split_name(name) # :nodoc: - raise Gem::Package::TooLongFileName if name.size > 256 + raise Gem::Package::TooLongFileName if name.bytesize > 256 - if name.size <= 100 then + if name.bytesize <= 100 then prefix = "" else parts = name.split(/\//) @@ -222,14 +278,14 @@ class Gem::Package::TarWriter loop do nxt = parts.pop - break if newname.size + 1 + nxt.size > 100 + break if newname.bytesize + 1 + nxt.bytesize > 100 newname = nxt + "/" + newname end prefix = (parts + [nxt]).join "/" name = newname - if name.size > 100 or prefix.size > 155 then + if name.bytesize > 100 or prefix.bytesize > 155 then raise Gem::Package::TooLongFileName end end diff --git a/lib/rubygems/package_task.rb b/lib/rubygems/package_task.rb index fe32a03b27..463f8d32cc 100644 --- a/lib/rubygems/package_task.rb +++ b/lib/rubygems/package_task.rb @@ -20,6 +20,7 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. require 'rubygems' +require 'rubygems/package' begin gem 'rake' rescue Gem::LoadError @@ -43,13 +44,10 @@ require 'rake/packagetask' # require 'rubygems/package_task' # # spec = Gem::Specification.new do |s| -# s.platform = Gem::Platform::RUBY # s.summary = "Ruby based make-like utility." # s.name = 'rake' # s.version = PKG_VERSION # s.requirements << 'none' -# s.require_path = 'lib' -# s.autorequire = 'rake' # s.files = PKG_FILES # s.description = <<-EOF # Rake is a Make-like program implemented in Ruby. Tasks @@ -113,7 +111,8 @@ class Gem::PackageTask < Rake::PackageTask file gem_path => [package_dir, gem_dir] + @gem_spec.files do chdir(gem_dir) do when_writing "Creating #{gem_spec.file_name}" do - Gem::Builder.new(gem_spec).build + Gem::Package.build gem_spec + verbose trace do mv gem_file, '..' end diff --git a/lib/rubygems/path_support.rb b/lib/rubygems/path_support.rb index 0aaf2c1bed..059e372112 100644 --- a/lib/rubygems/path_support.rb +++ b/lib/rubygems/path_support.rb @@ -1,4 +1,5 @@ ## +# # Gem::PathSupport facilitates the GEM_HOME and GEM_PATH environment settings # to the rest of RubyGems. # @@ -42,16 +43,18 @@ class Gem::PathSupport # Set the Gem search path (as reported by Gem.path). def path=(gpaths) - gem_path = [@home] + # FIX: it should be [home, *path], not [*path, home] + + gem_path = [] # FIX: I can't tell wtf this is doing. gpaths ||= (ENV['GEM_PATH'] || "").empty? ? nil : ENV["GEM_PATH"] - if gpaths then - if gpaths.kind_of?(Array) then - gem_path.push(*gpaths) + if gpaths + if gpaths.kind_of?(Array) + gem_path = gpaths.dup else - gem_path.push(*gpaths.split(File::PATH_SEPARATOR)) + gem_path = gpaths.split(File::PATH_SEPARATOR) end if File::ALT_SEPARATOR then @@ -59,10 +62,14 @@ class Gem::PathSupport this_path.gsub File::ALT_SEPARATOR, File::SEPARATOR end end + + gem_path << @home else - gem_path.push(*Gem.default_path) + gem_path = Gem.default_path + [@home] - gem_path << APPLE_GEM_HOME if defined?(APPLE_GEM_HOME) + if defined?(APPLE_GEM_HOME) + gem_path << APPLE_GEM_HOME + end end @path = gem_path.uniq diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb index f67e4022b6..7301153c4b 100644 --- a/lib/rubygems/platform.rb +++ b/lib/rubygems/platform.rb @@ -21,7 +21,8 @@ class Gem::Platform def self.match(platform) Gem.platforms.any? do |local_platform| - platform.nil? or local_platform == platform or + platform.nil? or + local_platform == platform or (local_platform != Gem::Platform::RUBY and local_platform =~ platform) end end @@ -65,28 +66,28 @@ class Gem::Platform @cpu, os = nil, cpu if os.nil? # legacy jruby @os, @version = case os - when /aix(\d+)/ then [ 'aix', $1 ] - when /cygwin/ then [ 'cygwin', nil ] - when /darwin(\d+)?/ then [ 'darwin', $1 ] - when /^macruby$/ then [ 'macruby', nil ] - when /freebsd(\d+)/ then [ 'freebsd', $1 ] - when /hpux(\d+)/ then [ 'hpux', $1 ] - when /^java$/, /^jruby$/ then [ 'java', nil ] - when /^java([\d.]*)/ then [ 'java', $1 ] - when /^dotnet$/ then [ 'dotnet', nil ] - when /^dotnet([\d.]*)/ then [ 'dotnet', $1 ] - when /linux/ then [ 'linux', $1 ] - when /mingw32/ then [ 'mingw32', nil ] + when /aix(\d+)?/ then [ 'aix', $1 ] + when /cygwin/ then [ 'cygwin', nil ] + when /darwin(\d+)?/ then [ 'darwin', $1 ] + when /^macruby$/ then [ 'macruby', nil ] + when /freebsd(\d+)?/ then [ 'freebsd', $1 ] + when /hpux(\d+)?/ then [ 'hpux', $1 ] + when /^java$/, /^jruby$/ then [ 'java', nil ] + when /^java([\d.]*)/ then [ 'java', $1 ] + when /^dotnet$/ then [ 'dotnet', nil ] + when /^dotnet([\d.]*)/ then [ 'dotnet', $1 ] + when /linux/ then [ 'linux', $1 ] + when /mingw32/ then [ 'mingw32', nil ] when /(mswin\d+)(\_(\d+))?/ then os, version = $1, $3 @cpu = 'x86' if @cpu.nil? and os =~ /32$/ [os, version] - when /(netbsd[a-z]*)(\d+)/ then [ $1, $2 ] - when /openbsd(\d+\.\d+)/ then [ 'openbsd', $1 ] - when /solaris(\d+\.\d+)/ then [ 'solaris', $1 ] + when /netbsdelf/ then [ 'netbsdelf', nil ] + when /openbsd(\d+\.\d+)?/ then [ 'openbsd', $1 ] + when /solaris(\d+\.\d+)?/ then [ 'solaris', $1 ] # test - when /^(\w+_platform)(\d+)/ then [ $1, $2 ] - else [ 'unknown', nil ] + when /^(\w+_platform)(\d+)?/ then [ $1, $2 ] + else [ 'unknown', nil ] end when Gem::Platform then @cpu = arch.cpu @@ -109,10 +110,6 @@ class Gem::Platform to_a.compact.join '-' end - def empty? - to_s.empty? - end - ## # Is +other+ equal to this platform? Two platforms are equal if they have # the same CPU, OS and version. @@ -186,9 +183,5 @@ class Gem::Platform # This will be replaced with Gem::Platform::local. CURRENT = 'current' - - extend Gem::Deprecate - - deprecate :empty?, :none, 2011, 11 end diff --git a/lib/rubygems/rdoc.rb b/lib/rubygems/rdoc.rb new file mode 100644 index 0000000000..65414462fa --- /dev/null +++ b/lib/rubygems/rdoc.rb @@ -0,0 +1,316 @@ +require 'rubygems' +require 'rubygems/user_interaction' +require 'fileutils' + +begin + gem 'rdoc' +rescue Gem::LoadError + # swallow +else + # This will force any deps that 'rdoc' might have + # (such as json) that are ambigious to be activated, which + # is important because we end up using Specification.reset + # and we don't want the warning it pops out. + Gem.finish_resolve +end + +loaded_hook = false + +begin + require 'rdoc/rubygems_hook' + loaded_hook = true + module Gem + RDoc = RDoc::RubygemsHook + end +rescue LoadError +end + +## +# Gem::RDoc provides methods to generate RDoc and ri data for installed gems. +# It works for RDoc 1.0.1 (in Ruby 1.8) up to RDoc 3.6. +# +# This implementation is considered obsolete. The RDoc project is the +# appropriate location to find this functionality. This file provides the +# hooks to load RDoc generation code from the "rdoc" gem and a fallback in +# case the installed version of RDoc does not have them. + +class Gem::RDoc # :nodoc: all + + include Gem::UserInteraction + + @rdoc_version = nil + @specs = [] + + ## + # Force installation of documentation? + + attr_accessor :force + + ## + # Generate rdoc? + + attr_accessor :generate_rdoc + + ## + # Generate ri data? + + attr_accessor :generate_ri + + class << self + + ## + # Loaded version of RDoc. Set by ::load_rdoc + + attr_reader :rdoc_version + + end + + ## + # Post installs hook that generates documentation for each specification in + # +specs+ + + def self.generation_hook installer, specs + types = installer.document + + generate_rdoc = types.include? 'rdoc' + generate_ri = types.include? 'ri' + + specs.each do |spec| + new(spec, generate_rdoc, generate_ri).generate + end + end + + ## + # Loads the RDoc generator + + def self.load_rdoc + return if @rdoc_version + + begin + require 'rdoc/rdoc' + + @rdoc_version = if ::RDoc.const_defined? :VERSION then + Gem::Version.new ::RDoc::VERSION + else + Gem::Version.new '1.0.1' + end + + rescue LoadError => e + raise Gem::DocumentError, "RDoc is not installed: #{e}" + end + end + + ## + # Creates a new documentation generator for +spec+. RDoc and ri data + # generation can be enabled or disabled through +generate_rdoc+ and + # +generate_ri+ respectively. + # + # Only +generate_ri+ is enabled by default. + + def initialize spec, generate_rdoc = false, generate_ri = true + @doc_dir = spec.doc_dir + @file_info = nil + @force = false + @rdoc = nil + @spec = spec + + @generate_rdoc = generate_rdoc + @generate_ri = generate_ri + + @rdoc_dir = spec.doc_dir 'rdoc' + @ri_dir = spec.doc_dir 'ri' + end + + ## + # Removes legacy rdoc arguments from +args+ + + def delete_legacy_args args + args.delete '--inline-source' + args.delete '--promiscuous' + args.delete '-p' + args.delete '--one-file' + end + + ## + # Generates documentation using the named +generator+ ("darkfish" or "ri") + # and following the given +options+. + # + # Documentation will be generated into +destination+ + + def document generator, options, destination + options = options.dup + options.exclude ||= [] # TODO maybe move to RDoc::Options#finish + options.setup_generator generator + options.op_dir = destination + options.finish + + @rdoc.options = options + @rdoc.generator = options.generator.new options + + say "Installing #{generator} documentation for #{@spec.full_name}" + + FileUtils.mkdir_p options.op_dir + + Dir.chdir options.op_dir do + begin + @rdoc.class.current = @rdoc + @rdoc.generator.generate @file_info + ensure + @rdoc.class.current = nil + end + end + end + + ## + # Generates RDoc and ri data + + def generate + return unless @generate_ri or @generate_rdoc + + setup + + if Gem::Requirement.new('< 3').satisfied_by? self.class.rdoc_version then + generate_legacy + else + ::RDoc::TopLevel.reset # TODO ::RDoc::RDoc.reset + ::RDoc::Parser::C.reset + + options = ::RDoc::Options.new + options.default_title = "#{@spec.full_name} Documentation" + options.files = [] + options.files.push(*@spec.require_paths) + options.files.push(*@spec.extra_rdoc_files) + + args = @spec.rdoc_options + + case config_args = Gem.configuration[:rdoc] + when String then + args = args.concat config_args.split + when Array then + args = args.concat config_args + end + + delete_legacy_args args + options.parse args + options.quiet = !Gem.configuration.really_verbose + + @rdoc = new_rdoc + @rdoc.options = options + + Dir.chdir @spec.full_gem_path do + @file_info = @rdoc.parse_files options.files + end + + document 'ri', options, @ri_dir if + @generate_ri and (@force or not File.exist? @ri_dir) + + document 'darkfish', options, @rdoc_dir if + @generate_rdoc and (@force or not File.exist? @rdoc_dir) + end + end + + ## + # Generates RDoc and ri data for legacy RDoc versions. This method will not + # exist in future versions. + + def generate_legacy + if @generate_rdoc then + FileUtils.rm_rf @rdoc_dir + say "Installing RDoc documentation for #{@spec.full_name}" + legacy_rdoc '--op', @rdoc_dir + end + + if @generate_ri then + FileUtils.rm_rf @ri_dir + say "Installing ri documentation for #{@spec.full_name}" + legacy_rdoc '--ri', '--op', @ri_dir + end + end + + ## + # Generates RDoc using a legacy version of RDoc from the ARGV-like +args+. + # This method will not exist in future versions. + + def legacy_rdoc *args + args << @spec.rdoc_options + args << '--quiet' + args << @spec.require_paths.clone + args << @spec.extra_rdoc_files + args << '--title' << "#{@spec.full_name} Documentation" + args = args.flatten.map do |arg| arg.to_s end + + delete_legacy_args args if + Gem::Requirement.new('>= 2.4.0') =~ self.class.rdoc_version + + r = new_rdoc + say "rdoc #{args.join ' '}" if Gem.configuration.really_verbose + + Dir.chdir @spec.full_gem_path do + begin + r.document args + rescue Errno::EACCES => e + dirname = File.dirname e.message.split("-")[1].strip + raise Gem::FilePermissionError, dirname + rescue Interrupt => e + raise e + rescue Exception => ex + alert_error "While generating documentation for #{@spec.full_name}" + ui.errs.puts "... MESSAGE: #{ex}" + ui.errs.puts "... RDOC args: #{args.join(' ')}" + ui.backtrace ex + ui.errs.puts "(continuing with the rest of the installation)" + ensure + end + end + end + + ## + # #new_rdoc creates a new RDoc instance. This method is provided only to + # make testing easier. + + def new_rdoc + ::RDoc::RDoc.new + end + + ## + # Is rdoc documentation installed? + + def rdoc_installed? + File.exist? @rdoc_dir + end + + ## + # Removes generated RDoc and ri data + + def remove + base_dir = @spec.base_dir + + raise Gem::FilePermissionError, base_dir unless File.writable? base_dir + + FileUtils.rm_rf @rdoc_dir + FileUtils.rm_rf @ri_dir + end + + ## + # Is ri data installed? + + def ri_installed? + File.exist? @ri_dir + end + + ## + # Prepares the spec for documentation generation + + def setup + self.class.load_rdoc + + raise Gem::FilePermissionError, @doc_dir if + File.exist?(@doc_dir) and not File.writable?(@doc_dir) + + FileUtils.mkdir_p @doc_dir unless File.exist? @doc_dir + end + +end unless loaded_hook + +Gem.done_installing(&Gem::RDoc.method(:generation_hook)) + diff --git a/lib/rubygems/remote_fetcher.rb b/lib/rubygems/remote_fetcher.rb index 37699d17fc..4bb2e3604f 100644 --- a/lib/rubygems/remote_fetcher.rb +++ b/lib/rubygems/remote_fetcher.rb @@ -1,6 +1,7 @@ require 'rubygems' require 'rubygems/user_interaction' require 'uri' +require 'resolv' ## # RemoteFetcher handles the details of fetching gems and gem information from @@ -8,8 +9,6 @@ require 'uri' class Gem::RemoteFetcher - BuiltinSSLCerts = File.expand_path("./ssl_certs/*.pem", File.dirname(__FILE__)) - include Gem::UserInteraction ## @@ -34,6 +33,13 @@ class Gem::RemoteFetcher end + ## + # A FetchError that indicates that the reason for not being + # able to fetch data was that the host could not be contacted + + class UnknownHostError < FetchError + end + @fetcher = nil ## @@ -53,8 +59,11 @@ class Gem::RemoteFetcher # * nil: respect environment variables (HTTP_PROXY, HTTP_PROXY_USER, # HTTP_PROXY_PASS) # * :no_proxy: ignore environment variables and _don't_ use a proxy + # + # +dns+: An object to use for DNS resolution of the API endpoint. + # By default, use Resolv::DNS. - def initialize(proxy = nil) + def initialize(proxy=nil, dns=Resolv::DNS.new) require 'net/http' require 'stringio' require 'time' @@ -72,6 +81,27 @@ class Gem::RemoteFetcher else URI.parse(proxy) end @user_agent = user_agent + @env_no_proxy = get_no_proxy_from_env + + @dns = dns + end + + ## + # + # Given a source at +uri+, calculate what hostname to actually + # connect to query the data for it. + + def api_endpoint(uri) + host = uri.host + + begin + res = @dns.getresource "_rubygems._tcp.#{host}", + Resolv::DNS::Resource::IN::SRV + rescue Resolv::ResolvError + uri + else + URI.parse "#{res.target}#{uri.path}" + end end ## @@ -82,14 +112,13 @@ class Gem::RemoteFetcher # larger, more emcompassing effort. -erikh def download_to_cache dependency - found = Gem::SpecFetcher.fetcher.fetch dependency, true, true, - dependency.prerelease? + found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dependency return if found.empty? - spec, source_uri = found.sort_by { |(s,_)| s.version }.last + spec, source = found.sort_by { |(s,_)| s.version }.last - download spec, source_uri + download spec, source.uri.to_s end ## @@ -100,11 +129,14 @@ class Gem::RemoteFetcher def download(spec, source_uri, install_dir = Gem.dir) Gem.ensure_gem_subdirectories(install_dir) rescue nil - if File.writable?(install_dir) - cache_dir = File.join install_dir, "cache" - else - cache_dir = File.join Gem.user_dir, "cache" - end + cache_dir = + if Dir.pwd == install_dir then # see fetch_command + install_dir + elsif File.writable? install_dir then + File.join install_dir, "cache" + else + File.join Gem.user_dir, "cache" + end gem_file_name = File.basename spec.cache_file local_gem_path = File.join cache_dir, gem_file_name @@ -123,6 +155,8 @@ class Gem::RemoteFetcher # URI.parse gets confused by MS Windows paths with forward slashes. scheme = nil if scheme =~ /^[a-z]$/i + # REFACTOR: split this up and dispatch on scheme (eg download_http) + # REFACTOR: be sure to clean up fake fetcher when you do this... cleaner case scheme when 'http', 'https' then unless File.exist? local_gem_path then @@ -132,7 +166,7 @@ class Gem::RemoteFetcher remote_gem_path = source_uri + "gems/#{gem_file_name}" - gem = self.fetch_path remote_gem_path + self.cache_update_path remote_gem_path, local_gem_path rescue Gem::RemoteFetcher::FetchError raise if spec.original_platform == spec.platform @@ -143,11 +177,7 @@ class Gem::RemoteFetcher remote_gem_path = source_uri + "gems/#{alternate_name}" - gem = self.fetch_path remote_gem_path - end - - File.open local_gem_path, 'wb' do |fp| - fp.write gem + self.cache_update_path remote_gem_path, local_gem_path end end when 'file' then @@ -184,7 +214,7 @@ class Gem::RemoteFetcher say "Using local gem #{local_gem_path}" if Gem.configuration.really_verbose else - raise Gem::InstallError, "unsupported URI scheme #{source_uri.scheme}" + raise ArgumentError, "unsupported URI scheme #{source_uri.scheme}" end local_gem_path @@ -232,18 +262,54 @@ class Gem::RemoteFetcher uri = URI.parse uri unless URI::Generic === uri raise ArgumentError, "bad uri: #{uri}" unless uri - raise ArgumentError, "uri scheme is invalid: #{uri.scheme.inspect}" unless - uri.scheme + + unless uri.scheme + raise ArgumentError, "uri scheme is invalid: #{uri.scheme.inspect}" + end data = send "fetch_#{uri.scheme}", uri, mtime, head - data = Gem.gunzip data if data and not head and uri.to_s =~ /gz$/ + + if data and !head and uri.to_s =~ /gz$/ + begin + data = Gem.gunzip data + rescue Zlib::GzipFile::Error + raise FetchError.new("server did not return a valid file", uri.to_s) + end + end + data rescue FetchError raise rescue Timeout::Error - raise FetchError.new('timed out', uri.to_s) + raise UnknownHostError.new('timed out', uri.to_s) rescue IOError, SocketError, SystemCallError => e - raise FetchError.new("#{e.class}: #{e}", uri.to_s) + if e.message =~ /getaddrinfo/ + raise UnknownHostError.new('no such name', uri.to_s) + else + raise FetchError.new("#{e.class}: #{e}", uri.to_s) + end + end + + ## + # Downloads +uri+ to +path+ if necessary. If no path is given, it just + # passes the data. + + def cache_update_path(uri, path = nil) + mtime = path && File.stat(path).mtime rescue nil + + if mtime && Net::HTTPNotModified === fetch_path(uri, mtime, true) + Gem.read_binary(path) + else + data = fetch_path(uri) + + if path + open(path, 'wb') do |io| + io.write data + end + end + + data + end end ## @@ -273,6 +339,17 @@ class Gem::RemoteFetcher URI end + ## + # Returns list of no_proxy entries (if any) from the environment + + def get_no_proxy_from_env + env_no_proxy = ENV['no_proxy'] || ENV['NO_PROXY'] + + return [] if env_no_proxy.nil? or env_no_proxy.empty? + + env_no_proxy.split(/\s*,\s*/) + end + ## # Returns an HTTP proxy URI if one is set in the environment variables. @@ -296,7 +373,7 @@ class Gem::RemoteFetcher # Normalize the URI by adding "http://" if it is missing. def normalize_uri(uri) - (uri =~ /^(https?|ftp|file):/) ? uri : "http://#{uri}" + (uri =~ /^(https?|ftp|file):/i) ? uri : "http://#{uri}" end ## @@ -306,7 +383,7 @@ class Gem::RemoteFetcher def connection_for(uri) net_http_args = [uri.host, uri.port] - if @proxy_uri then + if @proxy_uri and not no_proxy?(uri.host) then net_http_args += [ @proxy_uri.host, @proxy_uri.port, @@ -319,37 +396,23 @@ class Gem::RemoteFetcher @connections[connection_id] ||= Net::HTTP.new(*net_http_args) connection = @connections[connection_id] - if https?(uri) and !connection.started? then + if https?(uri) and not connection.started? then configure_connection_for_https(connection) - - # Don't refactor this with the else branch. We don't want the - # http-only code path to not depend on anything in OpenSSL. - # - begin - connection.start - rescue OpenSSL::SSL::SSLError, Errno::EHOSTDOWN => e - raise FetchError.new(e.message, uri) - end - else - begin - connection.start unless connection.started? - rescue Errno::EHOSTDOWN => e - raise FetchError.new(e.message, uri) - end end + connection.start unless connection.started? + connection + rescue OpenSSL::SSL::SSLError, Errno::EHOSTDOWN => e + raise FetchError.new(e.message, uri) end def configure_connection_for_https(connection) require 'net/https' - connection.use_ssl = true connection.verify_mode = Gem.configuration.ssl_verify_mode || OpenSSL::SSL::VERIFY_PEER - store = OpenSSL::X509::Store.new - if Gem.configuration.ssl_ca_cert if File.directory? Gem.configuration.ssl_ca_cert store.add_path Gem.configuration.ssl_ca_cert @@ -360,12 +423,12 @@ class Gem::RemoteFetcher store.set_default_paths add_rubygems_trusted_certs(store) end - connection.cert_store = store end def add_rubygems_trusted_certs(store) - Dir.glob(BuiltinSSLCerts).each do |ssl_cert_file| + pattern = File.expand_path("./ssl_certs/*.pem", File.dirname(__FILE__)) + Dir.glob(pattern).each do |ssl_cert_file| store.add_file ssl_cert_file end end @@ -378,13 +441,13 @@ class Gem::RemoteFetcher end end - ## - # Read the data from the (source based) URI, but if it is a file:// URI, - # read from the filesystem instead. - - def open_uri_or_path(uri, last_modified = nil, head = false, depth = 0) - raise "NO: Use fetch_path instead" - # TODO: deprecate for fetch_path + def no_proxy? host + host = host.downcase + @env_no_proxy.each do |pattern| + pattern = pattern.downcase + return true if host[-pattern.length, pattern.length ] == pattern + end + return false end ## diff --git a/lib/rubygems/request_set.rb b/lib/rubygems/request_set.rb new file mode 100644 index 0000000000..6c52b90c40 --- /dev/null +++ b/lib/rubygems/request_set.rb @@ -0,0 +1,182 @@ +require 'rubygems' +require 'rubygems/dependency' +require 'rubygems/dependency_resolver' +require 'rubygems/dependency_list' +require 'rubygems/installer' +require 'tsort' + +module Gem + class RequestSet + + include TSort + + def initialize(*deps) + @dependencies = deps + + yield self if block_given? + end + + attr_reader :dependencies + + # Declare that a gem of name +name+ with +reqs+ requirements + # is needed. + # + def gem(name, *reqs) + @dependencies << Gem::Dependency.new(name, reqs) + end + + # Add +deps+ Gem::Depedency objects to the set. + # + def import(deps) + @dependencies += deps + end + + # Resolve the requested dependencies and return an Array of + # Specification objects to be activated. + # + def resolve(set=nil) + r = Gem::DependencyResolver.new(@dependencies, set) + @requests = r.resolve + end + + # Resolve the requested dependencies against the gems + # available via Gem.path and return an Array of Specification + # objects to be activated. + # + def resolve_current + resolve DependencyResolver::CurrentSet.new + end + + # Load a dependency management file. + # + def load_gemdeps(path) + gf = GemDepedencyAPI.new(self, path) + gf.load + end + + def specs + @specs ||= @requests.map { |r| r.full_spec } + end + + def tsort_each_node(&block) + @requests.each(&block) + end + + def tsort_each_child(node) + node.spec.dependencies.each do |dep| + next if dep.type == :development + + match = @requests.find { |r| dep.match? r.spec.name, r.spec.version } + if match + begin + yield match + rescue TSort::Cyclic + end + else + raise Gem::DependencyError, "Unresolved depedency found during sorting - #{dep}" + end + end + end + + def sorted_requests + @sorted ||= strongly_connected_components.flatten + end + + def specs_in(dir) + Dir["#{dir}/specifications/*.gemspec"].map do |g| + Gem::Specification.load g + end + end + + def install_into(dir, force=true, &b) + existing = force ? [] : specs_in(dir) + + dir = File.expand_path dir + + installed = [] + + sorted_requests.each do |req| + if existing.find { |s| s.full_name == req.spec.full_name } + b.call req, nil if b + next + end + + path = req.download(dir) + + inst = Gem::Installer.new path, :install_dir => dir, + :only_install_dir => true + + b.call req, inst if b + + inst.install + + installed << req + end + + installed + end + + def install(options, &b) + if dir = options[:install_dir] + return install_into(dir, false, &b) + end + + cache_dir = options[:cache_dir] || Gem.dir + + specs = [] + + sorted_requests.each do |req| + if req.installed? + b.call req, nil if b + next + end + + path = req.download cache_dir + + inst = Gem::Installer.new path, options + + b.call req, inst if b + + specs << inst.install + end + + specs + end + + # A semi-compatible DSL for Bundler's Gemfile format + # + class GemDepedencyAPI + def initialize(set, path) + @set = set + @path = path + end + + def load + instance_eval File.read(@path).untaint, @path, 1 + end + + # DSL + + def source(url) + end + + def gem(name, *reqs) + # Ignore the opts for now. + reqs.pop if reqs.last.kind_of?(Hash) + + @set.gem name, *reqs + end + + def platform(what) + if what == :ruby + yield + end + end + + alias_method :platforms, :platform + + def group(*what) + end + end + end +end diff --git a/lib/rubygems/require_paths_builder.rb b/lib/rubygems/require_paths_builder.rb deleted file mode 100644 index 23e974639f..0000000000 --- a/lib/rubygems/require_paths_builder.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'rubygems' - -# TODO: remove after 1.9.1 dropped -module Gem::RequirePathsBuilder - def write_require_paths_file_if_needed(spec = @spec, gem_home = @gem_home) - return if spec.require_paths == ["lib"] && - (spec.bindir.nil? || spec.bindir == "bin") - file_name = File.join(gem_home, 'gems', "#{@spec.full_name}", ".require_paths") - file_name.untaint - File.open(file_name, "w") do |file| - spec.require_paths.each do |path| - file.puts path - end - file.puts spec.bindir if spec.bindir - end - end -end if Gem::QUICKLOADER_SUCKAGE - diff --git a/lib/rubygems/requirement.rb b/lib/rubygems/requirement.rb index 7abff01c39..b80e8dca19 100644 --- a/lib/rubygems/requirement.rb +++ b/lib/rubygems/requirement.rb @@ -1,5 +1,3 @@ -require "rubygems/version" - ## # A Requirement is a set of one or more version restrictions. It supports a # few (=, !=, >, <, >=, <=, ~>) different restriction operators. @@ -13,14 +11,16 @@ require "rubygems/version" require "rubygems/version" require "rubygems/deprecate" -class Gem::Requirement - include Comparable +# If we're being loaded after yaml was already required, then +# load our yaml + workarounds now. +Gem.load_yaml if defined? ::YAML +class Gem::Requirement OPS = { #:nodoc: "=" => lambda { |v, r| v == r }, "!=" => lambda { |v, r| v != r }, - ">" => lambda { |v, r| v > r }, - "<" => lambda { |v, r| v < r }, + ">" => lambda { |v, r| v > r }, + "<" => lambda { |v, r| v < r }, ">=" => lambda { |v, r| v >= r }, "<=" => lambda { |v, r| v <= r }, "~>" => lambda { |v, r| v >= r && v.release < r.bump } @@ -29,6 +29,10 @@ class Gem::Requirement quoted = OPS.keys.map { |k| Regexp.quote k }.join "|" PATTERN = /\A\s*(#{quoted})?\s*(#{Gem::Version::VERSION_PATTERN})\s*\z/ + DefaultRequirement = [">=", Gem::Version.new(0)] + + class BadRequirementError < ArgumentError; end + ## # Factory method to create a Gem::Requirement object. Input may be # a Version, a String, or nil. Intended to simplify client code. @@ -36,6 +40,9 @@ class Gem::Requirement # If the input is "weird", the default version requirement is # returned. + # REFACTOR: There's no reason that this can't be unified with .new. + # .new is the standard Ruby factory method. + def self.create input case input when Gem::Requirement then @@ -53,10 +60,6 @@ class Gem::Requirement ## # A default "version requirement" can surely _only_ be '>= 0'. - #-- - # This comment once said: - # - # "A default "version requirement" can surely _only_ be '> 0'." def self.default new '>= 0' @@ -74,14 +77,23 @@ class Gem::Requirement # parse("1.0") # => ["=", "1.0"] # parse(Gem::Version.new("1.0")) # => ["=, "1.0"] + # REFACTOR: Little two element arrays like this have no real semantic + # value. I'd love to see something like this: + # Constraint = Struct.new(:operator, :version); (or similar) + # and have a Requirement be a list of Constraints. + def self.parse obj return ["=", obj] if Gem::Version === obj unless PATTERN =~ obj.to_s - raise ArgumentError, "Illformed requirement [#{obj.inspect}]" + raise BadRequirementError, "Illformed requirement [#{obj.inspect}]" end - [$1 || "=", Gem::Version.new($2)] + if $1 == ">=" && $2 == "0" + DefaultRequirement + else + [$1 || "=", Gem::Version.new($2)] + end end ## @@ -101,13 +113,23 @@ class Gem::Requirement requirements.compact! requirements.uniq! - requirements << ">= 0" if requirements.empty? - @none = (requirements == ">= 0") - @requirements = requirements.map! { |r| self.class.parse r } + if requirements.empty? + @requirements = [DefaultRequirement] + else + @requirements = requirements.map! { |r| self.class.parse r } + end end + ## + # true if this gem has no requirements. + + # FIX: maybe this should be using #default ? def none? - @none ||= (to_s == ">= 0") + if @requirements.size == 1 + @requirements[0] == DefaultRequirement + else + false + end end def as_list # :nodoc: @@ -135,6 +157,7 @@ class Gem::Requirement instance_variable_set "@#{ivar}", val end + Gem.load_yaml fix_syck_default_key_in_requirements end @@ -142,6 +165,18 @@ class Gem::Requirement yaml_initialize coder.tag, coder.map end + def to_yaml_properties + ["@requirements"] + end + + def encode_with(coder) + coder.add 'requirements', @requirements + end + + ## + # A requirement is a prerelease if any of the versions inside of it + # are prereleases + def prerelease? requirements.any? { |r| r.last.prerelease? } end @@ -156,6 +191,8 @@ class Gem::Requirement # True if +version+ satisfies this Requirement. def satisfied_by? version + raise ArgumentError, "Need a Gem::Version: #{version.inspect}" unless + Gem::Version === version # #28965: syck has a bug with unquoted '=' YAML.loading as YAML::DefaultKey requirements.all? { |op, rv| (OPS[op] || OPS["="]).call version, rv } end @@ -176,12 +213,14 @@ class Gem::Requirement as_list.join ", " end - def <=> other # :nodoc: - to_s <=> other.to_s + # DOC: this should probably be :nodoc'd + def == other + Gem::Requirement === other and to_s == other.to_s end private + # DOC: this should probably be :nodoc'd def fix_syck_default_key_in_requirements Gem.load_yaml @@ -194,11 +233,9 @@ class Gem::Requirement end end -# :stopdoc: -# Gem::Version::Requirement is used in a lot of old YAML specs. It's aliased -# here for backwards compatibility. I'd like to remove this, maybe in RubyGems -# 2.0. - -::Gem::Version::Requirement = ::Gem::Requirement -# :startdoc: +# This is needed for compatibility with older yaml +# gemspecs. +class Gem::Version + Requirement = Gem::Requirement +end diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb index f51da65b4b..bec30e9238 100644 --- a/lib/rubygems/security.rb +++ b/lib/rubygems/security.rb @@ -5,80 +5,89 @@ #++ require 'rubygems/exceptions' -require 'rubygems/gem_openssl' +require 'openssl' require 'fileutils' +## +# = Signing gems # -# = Signed Gems README -# -# == Table of Contents -# * Overview -# * Walkthrough -# * Command-Line Options -# * OpenSSL Reference -# * Bugs/TODO -# * About the Author -# -# == Overview -# -# Gem::Security implements cryptographic signatures in RubyGems. The section +# The Gem::Security implements cryptographic signatures for gems. The section # below is a step-by-step guide to using signed gems and generating your own. # # == Walkthrough # +# === Building your certificate +# # In order to start signing your gems, you'll need to build a private key and # a self-signed certificate. Here's how: # -# # build a private key and certificate for gemmaster@example.com -# $ gem cert --build gemmaster@example.com +# # build a private key and certificate for yourself: +# $ gem cert --build you@example.com # -# This could take anywhere from 5 seconds to 10 minutes, depending on the -# speed of your computer (public key algorithms aren't exactly the speediest -# crypto algorithms in the world). When it's finished, you'll see the files -# "gem-private_key.pem" and "gem-public_cert.pem" in the current directory. +# This could take anywhere from a few seconds to a minute or two, depending on +# the speed of your computer (public key algorithms aren't exactly the +# speediest crypto algorithms in the world). When it's finished, you'll see +# the files "gem-private_key.pem" and "gem-public_cert.pem" in the current +# directory. # -# First things first: take the "gem-private_key.pem" file and move it -# somewhere private, preferably a directory only you have access to, a floppy -# (yuck!), a CD-ROM, or something comparably secure. Keep your private key -# hidden; if it's compromised, someone can sign packages as you (note: PKI has -# ways of mitigating the risk of stolen keys; more on that later). +# First things first: Move both files to ~/.gem if you don't already have a +# key and certificate in that directory. Ensure the file permissions make the +# key unreadable by others (by default the file is saved securely). # -# Now, let's sign an existing gem. I'll be using my Imlib2-Ruby bindings, but -# you can use whatever gem you'd like. Open up your existing gemspec file and -# add the following lines: +# Keep your private key hidden; if it's compromised, someone can sign packages +# as you (note: PKI has ways of mitigating the risk of stolen keys; more on +# that later). # -# # signing key and certificate chain -# s.signing_key = '/mnt/floppy/gem-private_key.pem' -# s.cert_chain = ['gem-public_cert.pem'] +# === Signing Gems # -# (Be sure to replace "/mnt/floppy" with the ultra-secret path to your private -# key). +# In RubyGems 2 and newer there is no extra work to sign a gem. RubyGems will +# automatically find your key and certificate in your home directory and use +# them to sign newly packaged gems. # -# After that, go ahead and build your gem as usual. Congratulations, you've -# just built your first signed gem! If you peek inside your gem file, you'll -# see a couple of new files have been added: +# If your certificate is not self-signed (signed by a third party) RubyGems +# will attempt to load the certificate chain from the trusted certificates. +# Use gem cert --add signing_cert.pem to add your signers as +# trusted certificates. See below for further information on certificate +# chains. # -# $ tar tf tar tf Imlib2-Ruby-0.5.0.gem -# data.tar.gz -# data.tar.gz.sig +# If you build your gem it will automatically be signed. If you peek inside +# your gem file, you'll see a couple of new files have been added: +# +# $ tar tf your-gem-1.0.gem # metadata.gz -# metadata.gz.sig +# metadata.gz.sum +# metadata.gz.sig # metadata signature +# data.tar.gz +# data.tar.gz.sum +# data.tar.gz.sig # data signature +# +# === Manually signing gems +# +# If you wish to store your key in a separate secure location you'll need to +# set your gems up for signing by hand. To do this, set the +# signing_key and cert_chain in the gemspec before +# packaging your gem: +# +# s.signing_key = '/secure/path/to/gem-private_key.pem' +# s.cert_chain = %w[/secure/path/to/gem-public_cert.pem] +# +# When you package your gem with these options set RubyGems will automatically +# load your key and certificate from the secure paths. +# +# === Signed gems and security policies # # Now let's verify the signature. Go ahead and install the gem, but add the -# following options: "-P HighSecurity", like this: +# following options: -P HighSecurity, like this: # # # install the gem with using the security policy "HighSecurity" -# $ sudo gem install Imlib2-Ruby-0.5.0.gem -P HighSecurity +# $ sudo gem install your.gem -P HighSecurity # -# The -P option sets your security policy -- we'll talk about that in just a -# minute. Eh, what's this? +# The -P option sets your security policy -- we'll talk about +# that in just a minute. Eh, what's this? # -# Attempting local installation of 'Imlib2-Ruby-0.5.0.gem' -# ERROR: Error installing gem Imlib2-Ruby-0.5.0.gem[.gem]: Couldn't -# verify data signature: Untrusted Signing Chain Root: cert = -# '/CN=gemmaster/DC=example/DC=com', error = 'path -# "/root/.rubygems/trust/cert-15dbb43a6edf6a70a85d4e784e2e45312cff7030.pem" -# does not exist' +# $ gem install -P HighSecurity your-gem-1.0.gem +# ERROR: While executing gem ... (Gem::Security::Exception) +# root cert /CN=you/DC=example is not trusted # # The culprit here is the security policy. RubyGems has several different # security policies. Let's take a short break and go over the security @@ -111,46 +120,48 @@ require 'fileutils' # RubyGems will simply refuse to install the package. Oh well, maybe # he'll have better luck causing problems for CPAN users instead :). # -# So, the reason RubyGems refused to install our shiny new signed gem was -# because it was from an untrusted source. Well, my code is infallible -# (hah!), so I'm going to add myself as a trusted source. -# -# Here's how: +# The reason RubyGems refused to install your shiny new signed gem was because +# it was from an untrusted source. Well, your code is infallible (naturally), +# so you need to add yourself as a trusted source: # -# # add trusted certificate -# gem cert --add gem-public_cert.pem +# # add trusted certificate +# gem cert --add ~/.gem/gem-public_cert.pem # -# I've added my public certificate as a trusted source. Now I can install -# packages signed my private key without any hassle. Let's try the install -# command above again: +# You've now added your public certificate as a trusted source. Now you can +# install packages signed by your private key without any hassle. Let's try +# the install command above again: # # # install the gem with using the HighSecurity policy (and this time # # without any shenanigans) -# $ sudo gem install Imlib2-Ruby-0.5.0.gem -P HighSecurity +# $ gem install -P HighSecurity your-gem-1.0.gem +# Successfully installed your-gem-1.0 +# 1 gem installed # -# This time RubyGems should accept your signed package and begin installing. -# While you're waiting for RubyGems to work it's magic, have a look at some of -# the other security commands: +# This time RubyGems will accept your signed package and begin installing. # -# Usage: gem cert [options] +# While you're waiting for RubyGems to work it's magic, have a look at some of +# the other security commands by running gem help cert: # # Options: -# -a, --add CERT Add a trusted certificate. -# -l, --list List trusted certificates. -# -r, --remove STRING Remove trusted certificates containing STRING. -# -b, --build EMAIL_ADDR Build private key and self-signed certificate -# for EMAIL_ADDR. -# -C, --certificate CERT Certificate for --sign command. -# -K, --private-key KEY Private key for --sign command. -# -s, --sign NEWCERT Sign a certificate with my key and certificate. -# -# (By the way, you can pull up this list any time you'd like by typing "gem -# cert --help") -# -# Hmm. We've already covered the "--build" option, and the "--add", "--list", -# and "--remove" commands seem fairly straightforward; they allow you to add, -# list, and remove the certificates in your trusted certificate list. But -# what's with this "--sign" option? +# -a, --add CERT Add a trusted certificate. +# -l, --list [FILTER] List trusted certificates where the +# subject contains FILTER +# -r, --remove FILTER Remove trusted certificates where the +# subject contains FILTER +# -b, --build EMAIL_ADDR Build private key and self-signed +# certificate for EMAIL_ADDR +# -C, --certificate CERT Signing certificate for --sign +# -K, --private-key KEY Key for --sign or --build +# -s, --sign CERT Signs CERT with the key from -K +# and the certificate from -C +# +# We've already covered the --build option, and the +# --add, --list, and --remove commands +# seem fairly straightforward; they allow you to add, list, and remove the +# certificates in your trusted certificate list. But what's with this +# --sign option? +# +# === Certificate chains # # To answer that question, let's take a look at "certificate chains", a # concept I mentioned earlier. There are a couple of problems with @@ -172,134 +183,102 @@ require 'fileutils' # trust. Here's a hypothetical example of a trust hierarchy based (roughly) # on geography: # -# # -------------------------- -# | rubygems@rubyforge.org | +# | rubygems@rubygems.org | # -------------------------- # | # ----------------------------------- # | | # ---------------------------- ----------------------------- -# | seattle.rb@zenspider.com | | dcrubyists@richkilmer.com | +# | seattlerb@seattlerb.org | | dcrubyists@richkilmer.com | # ---------------------------- ----------------------------- # | | | | # --------------- ---------------- ----------- -------------- -# | alf@seattle | | bob@portland | | pabs@dc | | tomcope@dc | +# | drbrain | | zenspider | | pabs@dc | | tomcope@dc | # --------------- ---------------- ----------- -------------- # # -# Now, rather than having 4 trusted certificates (one for alf@seattle, -# bob@portland, pabs@dc, and tomecope@dc), a user could actually get by with 1 -# certificate: the "rubygems@rubyforge.org" certificate. Here's how it works: +# Now, rather than having 4 trusted certificates (one for drbrain, zenspider, +# pabs@dc, and tomecope@dc), a user could actually get by with one +# certificate, the "rubygems@rubygems.org" certificate. +# +# Here's how it works: +# +# I install "rdoc-3.12.gem", a package signed by "drbrain". I've never heard +# of "drbrain", but his certificate has a valid signature from the +# "seattle.rb@seattlerb.org" certificate, which in turn has a valid signature +# from the "rubygems@rubygems.org" certificate. Voila! At this point, it's +# much more reasonable for me to trust a package signed by "drbrain", because +# I can establish a chain to "rubygems@rubygems.org", which I do trust. # -# I install "Alf2000-Ruby-0.1.0.gem", a package signed by "alf@seattle". I've -# never heard of "alf@seattle", but his certificate has a valid signature from -# the "seattle.rb@zenspider.com" certificate, which in turn has a valid -# signature from the "rubygems@rubyforge.org" certificate. Voila! At this -# point, it's much more reasonable for me to trust a package signed by -# "alf@seattle", because I can establish a chain to "rubygems@rubyforge.org", -# which I do trust. +# === Signing certificates # -# And the "--sign" option allows all this to happen. A developer creates -# their build certificate with the "--build" option, then has their -# certificate signed by taking it with them to their next regional Ruby meetup -# (in our hypothetical example), and it's signed there by the person holding -# the regional RubyGems signing certificate, which is signed at the next -# RubyConf by the holder of the top-level RubyGems certificate. At each point -# the issuer runs the same command: +# The --sign option allows all this to happen. A developer +# creates their build certificate with the --build option, then +# has their certificate signed by taking it with them to their next regional +# Ruby meetup (in our hypothetical example), and it's signed there by the +# person holding the regional RubyGems signing certificate, which is signed at +# the next RubyConf by the holder of the top-level RubyGems certificate. At +# each point the issuer runs the same command: # # # sign a certificate with the specified key and certificate # # (note that this modifies client_cert.pem!) # $ gem cert -K /mnt/floppy/issuer-priv_key.pem -C issuer-pub_cert.pem # --sign client_cert.pem # -# Then the holder of issued certificate (in this case, our buddy -# "alf@seattle"), can start using this signed certificate to sign RubyGems. -# By the way, in order to let everyone else know about his new fancy signed -# certificate, "alf@seattle" would change his gemspec file to look like this: +# Then the holder of issued certificate (in this case, your buddy "drbrain"), +# can start using this signed certificate to sign RubyGems. By the way, in +# order to let everyone else know about his new fancy signed certificate, +# "drbrain" would save his newly signed certificate as +# ~/.gem/gem-public_cert.pem # -# # signing key (still kept in an undisclosed location!) -# s.signing_key = '/mnt/floppy/alf-private_key.pem' -# -# # certificate chain (includes the issuer certificate now too) -# s.cert_chain = ['/home/alf/doc/seattlerb-public_cert.pem', -# '/home/alf/doc/alf_at_seattle-public_cert.pem'] -# -# Obviously, this RubyGems trust infrastructure doesn't exist yet. Also, in -# the "real world" issuers actually generate the child certificate from a +# Obviously this RubyGems trust infrastructure doesn't exist yet. Also, in +# the "real world", issuers actually generate the child certificate from a # certificate request, rather than sign an existing certificate. And our # hypothetical infrastructure is missing a certificate revocation system. # These are that can be fixed in the future... # -# I'm sure your new signed gem has finished installing by now (unless you're -# installing rails and all it's dependencies, that is ;D). At this point you -# should know how to do all of these new and interesting things: +# At this point you should know how to do all of these new and interesting +# things: # # * build a gem signing key and certificate -# * modify your existing gems to support signing # * adjust your security policy # * modify your trusted certificate list # * sign a certificate # -# If you've got any questions, feel free to contact me at the email address -# below. The next couple of sections -# -# -# == Command-Line Options -# -# Here's a brief summary of the certificate-related command line options: -# -# gem install -# -P, --trust-policy POLICY Specify gem trust policy. -# -# gem cert -# -a, --add CERT Add a trusted certificate. -# -l, --list List trusted certificates. -# -r, --remove STRING Remove trusted certificates containing -# STRING. -# -b, --build EMAIL_ADDR Build private key and self-signed -# certificate for EMAIL_ADDR. -# -C, --certificate CERT Certificate for --sign command. -# -K, --private-key KEY Private key for --sign command. -# -s, --sign NEWCERT Sign a certificate with my key and -# certificate. -# -# A more detailed description of each options is available in the walkthrough -# above. -# # == Manually verifying signatures # # In case you don't trust RubyGems you can verify gem signatures manually: # # 1. Fetch and unpack the gem # -# gem fetch some_signed_gem -# tar -xf some_signed_gem-1.0.gem +# gem fetch some_signed_gem +# tar -xf some_signed_gem-1.0.gem # # 2. Grab the public key from the gemspec # -# gem spec some_signed_gem-1.0.gem cert_chain | \ -# ruby -pe 'sub(/^ +/, "")' > public_key.crt +# gem spec some_signed_gem-1.0.gem cert_chain | \ +# ruby -ryaml -e 'puts YAML.load_documents($stdin)' > public_key.crt # # 3. Generate a SHA1 hash of the data.tar.gz # -# openssl dgst -sha1 < data.tar.gz > my.hash +# openssl dgst -sha1 < data.tar.gz > my.hash # # 4. Verify the signature # -# openssl rsautl -verify -inkey public_key.crt -certin \ -# -in data.tar.gz.sig > verified.hash +# openssl rsautl -verify -inkey public_key.crt -certin \ +# -in data.tar.gz.sig > verified.hash # # 5. Compare your hash to the verified hash # -# diff -s verified.hash my.hash +# diff -s verified.hash my.hash # # 6. Repeat 5 and 6 with metadata.gz # # == OpenSSL Reference # -# The .pem files generated by --build and --sign are just basic OpenSSL PEM -# files. Here's a couple of useful commands for manipulating them: +# The .pem files generated by --build and --sign are PEM files. Here's a +# couple of useful OpenSSL commands for manipulating them: # # # convert a PEM format X509 certificate into DER format: # # (note: Windows .cer files are X509 certificates in DER format) @@ -321,8 +300,8 @@ require 'fileutils' # * There's no way to define a system-wide trust list. # * custom security policies (from a YAML file, etc) # * Simple method to generate a signed certificate request -# * Support for OCSP, SCVP, CRLs, or some other form of cert -# status check (list is in order of preference) +# * Support for OCSP, SCVP, CRLs, or some other form of cert status check +# (list is in order of preference) # * Support for encrypted private keys # * Some sort of semi-formal trust hierarchy (see long-winded explanation # above) @@ -332,17 +311,13 @@ require 'fileutils' # MediumSecurity and HighSecurity policies) # * Better explanation of X509 naming (ie, we don't have to use email # addresses) -# * Possible alternate signing mechanisms (eg, via PGP). this could be done -# pretty easily by adding a :signing_type attribute to the gemspec, then add -# the necessary support in other places # * Honor AIA field (see note about OCSP above) -# * Maybe honor restriction extensions? +# * Honor extension restrictions # * Might be better to store the certificate chain as a PKCS#7 or PKCS#12 -# file, instead of an array embedded in the metadata. ideas? -# * Possibly embed signature and key algorithms into metadata (right now -# they're assumed to be the same as what's set in Gem::Security::OPT) +# file, instead of an array embedded in the metadata. +# * Flexible signature and key algorithms, not hard-coded to RSA and SHA1. # -# == About the Author +# == Original author # # Paul Duncan # http://pablotron.org/ @@ -355,472 +330,237 @@ module Gem::Security class Exception < Gem::Exception; end ## - # Default options for most of the methods below - - OPT = { - # private key options - :key_algo => Gem::SSL::PKEY_RSA, - :key_size => 2048, - - # public cert options - :cert_age => 365 * 24 * 3600, # 1 year - :dgst_algo => Gem::SSL::DIGEST_SHA1, - - # x509 certificate extensions - :cert_exts => { - 'basicConstraints' => 'CA:FALSE', - 'subjectKeyIdentifier' => 'hash', - 'keyUsage' => 'keyEncipherment,dataEncipherment,digitalSignature', - }, - - # save the key and cert to a file in build_self_signed_cert()? - :save_key => true, - :save_cert => true, - - # if you define either of these, then they'll be used instead of - # the output_fmt macro below - :save_key_path => nil, - :save_cert_path => nil, - - # output name format for self-signed certs - :output_fmt => 'gem-%s.pem', - :munge_re => Regexp.new(/[^a-z0-9_.-]+/), - - # output directory for trusted certificate checksums - :trust_dir => File.join(Gem.user_home, '.gem', 'trust'), - - # default permissions for trust directory and certs - :perms => { - :trust_dir => 0700, - :trusted_cert => 0600, - :signing_cert => 0600, - :signing_key => 0600, - }, - } + # Digest algorithm used to sign gems + + DIGEST_ALGORITHM = OpenSSL::Digest::SHA1 ## - # A Gem::Security::Policy object encapsulates the settings for verifying - # signed gem files. This is the base class. You can either declare an - # instance of this or use one of the preset security policies below. - - class Policy - attr_accessor :verify_data, :verify_signer, :verify_chain, - :verify_root, :only_trusted, :only_signed - - # - # Create a new Gem::Security::Policy object with the given mode and - # options. - # - def initialize(policy = {}, opt = {}) - # set options - @opt = Gem::Security::OPT.merge(opt) - - # build policy - policy.each_pair do |key, val| - case key - when :verify_data then @verify_data = val - when :verify_signer then @verify_signer = val - when :verify_chain then @verify_chain = val - when :verify_root then @verify_root = val - when :only_trusted then @only_trusted = val - when :only_signed then @only_signed = val - end - end - end + # Used internally to select the signing digest from all computed digests - # - # Get the path to the file for this cert. - # - def self.trusted_cert_path(cert, opt = {}) - opt = Gem::Security::OPT.merge(opt) + DIGEST_NAME = DIGEST_ALGORITHM.new.name # :nodoc: - # get digest algorithm, calculate checksum of root.subject - algo = opt[:dgst_algo] - dgst = algo.hexdigest(cert.subject.to_s) + ## + # Algorithm for creating the key pair used to sign gems - # build path to trusted cert file - name = "cert-#{dgst}.pem" + KEY_ALGORITHM = OpenSSL::PKey::RSA - # join and return path components - File::join(opt[:trust_dir], name) - end + ## + # Length of keys created by KEY_ALGORITHM - # - # Verify that the gem data with the given signature and signing chain - # matched this security policy at the specified time. - # - def verify_gem(signature, data, chain, time = Time.now) - Gem.ensure_ssl_available - cert_class = OpenSSL::X509::Certificate - exc = Gem::Security::Exception - chain ||= [] - - chain = chain.map{ |str| cert_class.new(str) } - signer, ch_len = chain[-1], chain.size - - # make sure signature is valid - if @verify_data - # get digest algorithm (TODO: this should be configurable) - dgst = @opt[:dgst_algo] - - # verify the data signature (this is the most important part, so don't - # screw it up :D) - v = signer.public_key.verify(dgst.new, signature, data) - raise exc, "Invalid Gem Signature" unless v - - # make sure the signer is valid - if @verify_signer - # make sure the signing cert is valid right now - v = signer.check_validity(nil, time) - raise exc, "Invalid Signature: #{v[:desc]}" unless v[:is_valid] - end - end - - # make sure the certificate chain is valid - if @verify_chain - # iterate down over the chain and verify each certificate against it's - # issuer - (ch_len - 1).downto(1) do |i| - issuer, cert = chain[i - 1, 2] - v = cert.check_validity(issuer, time) - raise exc, "%s: cert = '%s', error = '%s'" % [ - 'Invalid Signing Chain', cert.subject, v[:desc] - ] unless v[:is_valid] - end - - # verify root of chain - if @verify_root - # make sure root is self-signed - root = chain[0] - raise exc, "%s: %s (subject = '%s', issuer = '%s')" % [ - 'Invalid Signing Chain Root', - 'Subject does not match Issuer for Gem Signing Chain', - root.subject.to_s, - root.issuer.to_s, - ] unless root.issuer.to_s == root.subject.to_s - - # make sure root is valid - v = root.check_validity(root, time) - raise exc, "%s: cert = '%s', error = '%s'" % [ - 'Invalid Signing Chain Root', root.subject, v[:desc] - ] unless v[:is_valid] - - # verify that the chain root is trusted - if @only_trusted - # get digest algorithm, calculate checksum of root.subject - algo = @opt[:dgst_algo] - path = Gem::Security::Policy.trusted_cert_path(root, @opt) - - # check to make sure trusted path exists - raise exc, "%s: cert = '%s', error = '%s'" % [ - 'Untrusted Signing Chain Root', - root.subject.to_s, - "path \"#{path}\" does not exist", - ] unless File.exist?(path) - - # load calculate digest from saved cert file - save_cert = OpenSSL::X509::Certificate.new(File.read(path)) - save_dgst = algo.digest(save_cert.public_key.to_s) - - # create digest of public key - pkey_str = root.public_key.to_s - cert_dgst = algo.digest(pkey_str) - - # now compare the two digests, raise exception - # if they don't match - raise exc, "%s: %s (saved = '%s', root = '%s')" % [ - 'Invalid Signing Chain Root', - "Saved checksum doesn't match root checksum", - save_dgst, cert_dgst, - ] unless save_dgst == cert_dgst - end - end - - # return the signing chain - chain.map { |cert| cert.subject } - end - end - end + KEY_LENGTH = 2048 ## - # No security policy: all package signature checks are disabled. + # One year in seconds - NoSecurity = Policy.new( - :verify_data => false, - :verify_signer => false, - :verify_chain => false, - :verify_root => false, - :only_trusted => false, - :only_signed => false - ) + ONE_YEAR = 86400 * 365 ## - # AlmostNo security policy: only verify that the signing certificate is the - # one that actually signed the data. Make no attempt to verify the signing - # certificate chain. + # The default set of extensions are: # - # This policy is basically useless. better than nothing, but can still be - # easily spoofed, and is not recommended. - - AlmostNoSecurity = Policy.new( - :verify_data => true, - :verify_signer => false, - :verify_chain => false, - :verify_root => false, - :only_trusted => false, - :only_signed => false - ) + # * The certificate is not a certificate authority + # * The key for the certificate may be used for key and data encipherment + # and digital signatures + # * The certificate contains a subject key identifier + + EXTENSIONS = { + 'basicConstraints' => 'CA:FALSE', + 'keyUsage' => + 'keyEncipherment,dataEncipherment,digitalSignature', + 'subjectKeyIdentifier' => 'hash', + } - ## - # Low security policy: only verify that the signing certificate is actually - # the gem signer, and that the signing certificate is valid. - # - # This policy is better than nothing, but can still be easily spoofed, and - # is not recommended. - - LowSecurity = Policy.new( - :verify_data => true, - :verify_signer => true, - :verify_chain => false, - :verify_root => false, - :only_trusted => false, - :only_signed => false - ) + def self.alt_name_or_x509_entry certificate, x509_entry + alt_name = certificate.extensions.find do |extension| + extension.oid == "#{x509_entry}AltName" + end - ## - # Medium security policy: verify the signing certificate, verify the signing - # certificate chain all the way to the root certificate, and only trust root - # certificates that we have explicitly allowed trust for. - # - # This security policy is reasonable, but it allows unsigned packages, so a - # malicious person could simply delete the package signature and pass the - # gem off as unsigned. - - MediumSecurity = Policy.new( - :verify_data => true, - :verify_signer => true, - :verify_chain => true, - :verify_root => true, - :only_trusted => true, - :only_signed => false - ) + return alt_name.value if alt_name + + certificate.send x509_entry + end ## - # High security policy: only allow signed gems to be installed, verify the - # signing certificate, verify the signing certificate chain all the way to - # the root certificate, and only trust root certificates that we have - # explicitly allowed trust for. + # Creates an unsigned certificate for +subject+ and +key+. The lifetime of + # the key is from the current time to +age+ which defaults to one year. # - # This security policy is significantly more difficult to bypass, and offers - # a reasonable guarantee that the contents of the gem have not been altered. - - HighSecurity = Policy.new( - :verify_data => true, - :verify_signer => true, - :verify_chain => true, - :verify_root => true, - :only_trusted => true, - :only_signed => true - ) + # The +extensions+ restrict the key to the indicated uses. - ## - # Hash of configured security policies - - Policies = { - 'NoSecurity' => NoSecurity, - 'AlmostNoSecurity' => AlmostNoSecurity, - 'LowSecurity' => LowSecurity, - 'MediumSecurity' => MediumSecurity, - 'HighSecurity' => HighSecurity, - } + def self.create_cert subject, key, age = ONE_YEAR, extensions = EXTENSIONS, + serial = 1 + cert = OpenSSL::X509::Certificate.new - ## - # Sign the cert cert with @signing_key and @signing_cert, using the digest - # algorithm opt[:dgst_algo]. Returns the newly signed certificate. + cert.public_key = key.public_key + cert.version = 2 + cert.serial = serial - def self.sign_cert(cert, signing_key, signing_cert, opt = {}) - opt = OPT.merge(opt) + cert.not_before = Time.now + cert.not_after = Time.now + age - cert.issuer = signing_cert.subject - cert.sign signing_key, opt[:dgst_algo].new + cert.subject = subject - cert - end + ef = OpenSSL::X509::ExtensionFactory.new nil, cert - ## - # Make sure the trust directory exists. If it does exist, make sure it's - # actually a directory. If not, then create it with the appropriate - # permissions. - - def self.verify_trust_dir(path, perms) - # if the directory exists, then make sure it is in fact a directory. if - # it doesn't exist, then create it with the appropriate permissions - if File.exist?(path) - # verify that the trust directory is actually a directory - unless File.directory?(path) - err = "trust directory #{path} isn't a directory" - raise Gem::Security::Exception, err - end - else - # trust directory doesn't exist, so create it with permissions - FileUtils.mkdir_p(path) - FileUtils.chmod(perms, path) + cert.extensions = extensions.map do |ext_name, value| + ef.create_extension ext_name, value end + + cert end ## - # Build a certificate from the given DN and private key. + # Creates a self-signed certificate with an issuer and subject from +email+, + # a subject alternative name of +email+ and the given +extensions+ for the + # +key+. - def self.build_cert(name, key, opt = {}) - Gem.ensure_ssl_available - opt = OPT.merge opt + def self.create_cert_email email, key, age = ONE_YEAR, extensions = EXTENSIONS + subject = email_to_name email - cert = OpenSSL::X509::Certificate.new + extensions = extensions.merge "subjectAltName" => "email:#{email}" - cert.not_after = Time.now + opt[:cert_age] - cert.not_before = Time.now - cert.public_key = key.public_key - cert.serial = 0 - cert.subject = name - cert.version = 2 + create_cert_self_signed subject, key, age, extensions + end - ef = OpenSSL::X509::ExtensionFactory.new nil, cert + ## + # Creates a self-signed certificate with an issuer and subject of +subject+ + # and the given +extensions+ for the +key+. - cert.extensions = opt[:cert_exts].map do |ext_name, value| - ef.create_extension ext_name, value - end + def self.create_cert_self_signed subject, key, age = ONE_YEAR, + extensions = EXTENSIONS, serial = 1 + certificate = create_cert subject, key, age, extensions - i_key = opt[:issuer_key] || key - i_cert = opt[:issuer_cert] || cert + sign certificate, key, certificate, age, extensions, serial + end - cert = sign_cert cert, i_key, i_cert, opt + ## + # Creates a new key pair of the specified +length+ and +algorithm+. The + # default is a 2048 bit RSA key. - cert + def self.create_key length = KEY_LENGTH, algorithm = KEY_ALGORITHM + algorithm.new length end ## - # Build a self-signed certificate for the given email address. + # Turns +email_address+ into an OpenSSL::X509::Name - def self.build_self_signed_cert(email_addr, opt = {}) - Gem.ensure_ssl_available - opt = OPT.merge(opt) - path = { :key => nil, :cert => nil } + def self.email_to_name email_address + email_address = email_address.gsub(/[^\w@.-]+/i, '_') - name = email_to_name email_addr, opt[:munge_re] + cn, dcs = email_address.split '@' - key = opt[:key_algo].new opt[:key_size] + dcs = dcs.split '.' - verify_trust_dir opt[:trust_dir], opt[:perms][:trust_dir] + name = "CN=#{cn}/#{dcs.map { |dc| "DC=#{dc}" }.join '/'}" - if opt[:save_key] then - path[:key] = opt[:save_key_path] || (opt[:output_fmt] % 'private_key') + OpenSSL::X509::Name.parse name + end - open path[:key], 'wb' do |io| - io.chmod opt[:perms][:signing_key] - io.write key.to_pem - end + ## + # Signs +expired_certificate+ with +private_key+ if the keys match and the + # expired certificate was self-signed. + #-- + # TODO increment serial + + def self.re_sign expired_certificate, private_key, age = ONE_YEAR, + extensions = EXTENSIONS + raise Gem::Security::Exception, + "incorrect signing key for re-signing " \ + "#{expired_certificate.subject}" unless + expired_certificate.public_key.to_pem == private_key.public_key.to_pem + + unless expired_certificate.subject.to_s == + expired_certificate.issuer.to_s then + subject = alt_name_or_x509_entry expired_certificate, :subject + issuer = alt_name_or_x509_entry expired_certificate, :issuer + + raise Gem::Security::Exception, + "#{subject} is not self-signed, contact #{issuer} " \ + "to obtain a valid certificate" end - cert = build_cert name, key, opt + serial = expired_certificate.serial + 1 - if opt[:save_cert] then - path[:cert] = opt[:save_cert_path] || (opt[:output_fmt] % 'public_cert') + create_cert_self_signed(expired_certificate.subject, private_key, age, + extensions, serial) + end - open path[:cert], 'wb' do |file| - file.chmod opt[:perms][:signing_cert] - file.write cert.to_pem - end - end + ## + # Resets the trust directory for verifying gems. - { :key => key, :cert => cert, - :key_path => path[:key], :cert_path => path[:cert] } + def self.reset + @trust_dir = nil end ## - # Turns +email_address+ into an OpenSSL::X509::Name + # Sign the public key from +certificate+ with the +signing_key+ and + # +signing_cert+, using the Gem::Security::DIGEST_ALGORITHM. Uses the + # default certificate validity range and extensions. + # + # Returns the newly signed certificate. - def self.email_to_name email_address, munge_re - cn, dcs = email_address.split '@' + def self.sign certificate, signing_key, signing_cert, + age = ONE_YEAR, extensions = EXTENSIONS, serial = 1 + signee_subject = certificate.subject + signee_key = certificate.public_key - dcs = dcs.split '.' + alt_name = certificate.extensions.find do |extension| + extension.oid == 'subjectAltName' + end - cn = cn.gsub munge_re, '_' + extensions = extensions.merge 'subjectAltName' => alt_name.value if + alt_name - dcs = dcs.map do |dc| - dc.gsub munge_re, '_' + issuer_alt_name = signing_cert.extensions.find do |extension| + extension.oid == 'subjectAltName' end - name = "CN=#{cn}/" << dcs.map { |dc| "DC=#{dc}" }.join('/') + extensions = extensions.merge 'issuerAltName' => issuer_alt_name.value if + issuer_alt_name - OpenSSL::X509::Name.parse name + signed = create_cert signee_subject, signee_key, age, extensions, serial + signed.issuer = signing_cert.subject + + signed.sign signing_key, Gem::Security::DIGEST_ALGORITHM.new end ## - # Add certificate to trusted cert list. - # - # Note: At the moment these are stored in OPT[:trust_dir], although that - # directory may change in the future. + # Returns a Gem::Security::TrustDir which wraps the directory where trusted + # certificates live. - def self.add_trusted_cert(cert, opt = {}) - opt = OPT.merge(opt) + def self.trust_dir + return @trust_dir if @trust_dir - # get destination path - path = Gem::Security::Policy.trusted_cert_path(cert, opt) + dir = File.join Gem.user_home, '.gem', 'trust' - # verify trust directory (can't write to nowhere, you know) - verify_trust_dir(opt[:trust_dir], opt[:perms][:trust_dir]) + @trust_dir ||= Gem::Security::TrustDir.new dir + end - # write cert to output file - File.open(path, 'wb') do |file| - file.chmod(opt[:perms][:trusted_cert]) - file.write(cert.to_pem) - end + ## + # Enumerates the trusted certificates via Gem::Security::TrustDir. - # return nil - nil + def self.trusted_certificates &block + trust_dir.each_certificate(&block) end ## - # Basic OpenSSL-based package signing class. - - class Signer - - attr_accessor :cert_chain - attr_accessor :key - - def initialize(key, cert_chain) - Gem.ensure_ssl_available - @algo = Gem::Security::OPT[:dgst_algo] - @key, @cert_chain = key, cert_chain - - # check key, if it's a file, and if it's key, leave it alone - if @key && !@key.kind_of?(OpenSSL::PKey::PKey) - @key = OpenSSL::PKey::RSA.new(File.read(@key)) - end - - # check cert chain, if it's a file, load it, if it's cert data, convert - # it into a cert object, and if it's a cert object, leave it alone - if @cert_chain - @cert_chain = @cert_chain.map do |cert| - # check cert, if it's a file, load it, if it's cert data, convert it - # into a cert object, and if it's a cert object, leave it alone - if cert && !cert.kind_of?(OpenSSL::X509::Certificate) - cert = File.read(cert) if File::exist?(cert) - cert = OpenSSL::X509::Certificate.new(cert) - end - cert - end - end - end + # Writes +pemmable+, which must respond to +to_pem+ to +path+ with the given + # +permissions+. - ## - # Sign data with given digest algorithm + def self.write pemmable, path, permissions = 0600 + path = File.expand_path path - def sign(data) - @key.sign(@algo.new, data) + open path, 'wb', permissions do |io| + io.write pemmable.to_pem end + path end + reset + end +require 'rubygems/security/policy' +require 'rubygems/security/policies' +require 'rubygems/security/signer' +require 'rubygems/security/trust_dir' + diff --git a/lib/rubygems/security/policies.rb b/lib/rubygems/security/policies.rb new file mode 100644 index 0000000000..a976ecaf59 --- /dev/null +++ b/lib/rubygems/security/policies.rb @@ -0,0 +1,115 @@ +module Gem::Security + + ## + # No security policy: all package signature checks are disabled. + + NoSecurity = Policy.new( + 'No Security', + :verify_data => false, + :verify_signer => false, + :verify_chain => false, + :verify_root => false, + :only_trusted => false, + :only_signed => false + ) + + ## + # AlmostNo security policy: only verify that the signing certificate is the + # one that actually signed the data. Make no attempt to verify the signing + # certificate chain. + # + # This policy is basically useless. better than nothing, but can still be + # easily spoofed, and is not recommended. + + AlmostNoSecurity = Policy.new( + 'Almost No Security', + :verify_data => true, + :verify_signer => false, + :verify_chain => false, + :verify_root => false, + :only_trusted => false, + :only_signed => false + ) + + ## + # Low security policy: only verify that the signing certificate is actually + # the gem signer, and that the signing certificate is valid. + # + # This policy is better than nothing, but can still be easily spoofed, and + # is not recommended. + + LowSecurity = Policy.new( + 'Low Security', + :verify_data => true, + :verify_signer => true, + :verify_chain => false, + :verify_root => false, + :only_trusted => false, + :only_signed => false + ) + + ## + # Medium security policy: verify the signing certificate, verify the signing + # certificate chain all the way to the root certificate, and only trust root + # certificates that we have explicitly allowed trust for. + # + # This security policy is reasonable, but it allows unsigned packages, so a + # malicious person could simply delete the package signature and pass the + # gem off as unsigned. + + MediumSecurity = Policy.new( + 'Medium Security', + :verify_data => true, + :verify_signer => true, + :verify_chain => true, + :verify_root => true, + :only_trusted => true, + :only_signed => false + ) + + ## + # High security policy: only allow signed gems to be installed, verify the + # signing certificate, verify the signing certificate chain all the way to + # the root certificate, and only trust root certificates that we have + # explicitly allowed trust for. + # + # This security policy is significantly more difficult to bypass, and offers + # a reasonable guarantee that the contents of the gem have not been altered. + + HighSecurity = Policy.new( + 'High Security', + :verify_data => true, + :verify_signer => true, + :verify_chain => true, + :verify_root => true, + :only_trusted => true, + :only_signed => true + ) + + ## + # Policy used to verify a certificate and key when signing a gem + + SigningPolicy = Policy.new( + 'Signing Policy', + :verify_data => false, + :verify_signer => true, + :verify_chain => true, + :verify_root => true, + :only_trusted => false, + :only_signed => false + ) + + ## + # Hash of configured security policies + + Policies = { + 'NoSecurity' => NoSecurity, + 'AlmostNoSecurity' => AlmostNoSecurity, + 'LowSecurity' => LowSecurity, + 'MediumSecurity' => MediumSecurity, + 'HighSecurity' => HighSecurity, + # SigningPolicy is not intended for use by `gem -P` so do not list it + } + +end + diff --git a/lib/rubygems/security/policy.rb b/lib/rubygems/security/policy.rb new file mode 100644 index 0000000000..d22b7ce5bd --- /dev/null +++ b/lib/rubygems/security/policy.rb @@ -0,0 +1,227 @@ +## +# A Gem::Security::Policy object encapsulates the settings for verifying +# signed gem files. This is the base class. You can either declare an +# instance of this or use one of the preset security policies in +# Gem::Security::Policies. + +class Gem::Security::Policy + + attr_reader :name + + attr_accessor :only_signed + attr_accessor :only_trusted + attr_accessor :verify_chain + attr_accessor :verify_data + attr_accessor :verify_root + attr_accessor :verify_signer + + ## + # Create a new Gem::Security::Policy object with the given mode and + # options. + + def initialize name, policy = {}, opt = {} + @name = name + + @opt = opt + + # Default to security + @only_signed = true + @only_trusted = true + @verify_chain = true + @verify_data = true + @verify_root = true + @verify_signer = true + + policy.each_pair do |key, val| + case key + when :verify_data then @verify_data = val + when :verify_signer then @verify_signer = val + when :verify_chain then @verify_chain = val + when :verify_root then @verify_root = val + when :only_trusted then @only_trusted = val + when :only_signed then @only_signed = val + end + end + end + + ## + # Verifies each certificate in +chain+ has signed the following certificate + # and is valid for the given +time+. + + def check_chain chain, time + chain.each_cons 2 do |issuer, cert| + check_cert cert, issuer, time + end + + true + rescue Gem::Security::Exception => e + raise Gem::Security::Exception, "invalid signing chain: #{e.message}" + end + + ## + # Verifies that +data+ matches the +signature+ created by +public_key+ and + # the +digest+ algorithm. + + def check_data public_key, digest, signature, data + raise Gem::Security::Exception, "invalid signature" unless + public_key.verify digest.new, signature, data.digest + + true + end + + ## + # Ensures that +signer+ is valid for +time+ and was signed by the +issuer+. + # If the +issuer+ is +nil+ no verification is performed. + + def check_cert signer, issuer, time + message = "certificate #{signer.subject}" + + if not_before = signer.not_before and not_before > time then + raise Gem::Security::Exception, + "#{message} not valid before #{not_before}" + end + + if not_after = signer.not_after and not_after < time then + raise Gem::Security::Exception, "#{message} not valid after #{not_after}" + end + + if issuer and not signer.verify issuer.public_key then + raise Gem::Security::Exception, + "#{message} was not issued by #{issuer.subject}" + end + + true + end + + ## + # Ensures the public key of +key+ matches the public key in +signer+ + + def check_key signer, key + raise Gem::Security::Exception, + "certificate #{signer.subject} does not match the signing key" unless + signer.public_key.to_pem == key.public_key.to_pem + + true + end + + ## + # Ensures the root certificate in +chain+ is self-signed and valid for + # +time+. + + def check_root chain, time + root = chain.first + + raise Gem::Security::Exception, + "root certificate #{root.subject} is not self-signed " \ + "(issuer #{root.issuer})" if + root.issuer.to_s != root.subject.to_s # HACK to_s is for ruby 1.8 + + check_cert root, root, time + end + + ## + # Ensures the root of +chain+ has a trusted certificate in +trust_dir+ and + # the digests of the two certificates match according to +digester+ + + def check_trust chain, digester, trust_dir + root = chain.first + + path = Gem::Security.trust_dir.cert_path root + + unless File.exist? path then + message = "root cert #{root.subject} is not trusted" + + message << " (root of signing cert #{chain.last.subject})" if + chain.length > 1 + + raise Gem::Security::Exception, message + end + + save_cert = OpenSSL::X509::Certificate.new File.read path + save_dgst = digester.digest save_cert.public_key.to_s + + pkey_str = root.public_key.to_s + cert_dgst = digester.digest pkey_str + + raise Gem::Security::Exception, + "trusted root certificate #{root.subject} checksum " \ + "does not match signing root certificate checksum" unless + save_dgst == cert_dgst + + true + end + + def inspect # :nodoc: + "[Policy: %s - data: %p signer: %p chain: %p root: %p " \ + "signed-only: %p trusted-only: %p]" % [ + @name, @verify_chain, @verify_data, @verify_root, @verify_signer, + @only_signed, @only_trusted, + ] + end + + ## + # Verifies the certificate +chain+ is valid, the +digests+ match the + # signatures +signatures+ created by the signer depending on the +policy+ + # settings. + # + # If +key+ is given it is used to validate the signing certificate. + + def verify chain, key = nil, digests = {}, signatures = {} + if @only_signed and signatures.empty? then + raise Gem::Security::Exception, + "unsigned gems are not allowed by the #{name} policy" + end + + opt = @opt + digester = Gem::Security::DIGEST_ALGORITHM + trust_dir = opt[:trust_dir] + time = Time.now + + signer_digests = digests.find do |algorithm, file_digests| + file_digests.values.first.name == Gem::Security::DIGEST_NAME + end + + signer_digests = digests.values.first || {} + + signer = chain.last + + check_key signer, key if key + + check_cert signer, nil, time if @verify_signer + + check_chain chain, time if @verify_chain + + check_root chain, time if @verify_root + + check_trust chain, digester, trust_dir if @only_trusted + + signer_digests.each do |file, digest| + signature = signatures[file] + + raise Gem::Security::Exception, "missing signature for #{file}" unless + signature + + check_data signer.public_key, digester, signature, digest if @verify_data + end + + true + end + + ## + # Extracts the certificate chain from the +spec+ and calls #verify to ensure + # the signatures and certificate chain is valid according to the policy.. + + def verify_signatures spec, digests, signatures + chain = spec.cert_chain.map do |cert_pem| + OpenSSL::X509::Certificate.new cert_pem + end + + verify chain, nil, digests, signatures + + true + end + + alias to_s name # :nodoc: + +end + diff --git a/lib/rubygems/security/signer.rb b/lib/rubygems/security/signer.rb new file mode 100644 index 0000000000..29b03683b7 --- /dev/null +++ b/lib/rubygems/security/signer.rb @@ -0,0 +1,136 @@ +## +# Basic OpenSSL-based package signing class. + +class Gem::Security::Signer + + ## + # The chain of certificates for signing including the signing certificate + + attr_accessor :cert_chain + + ## + # The private key for the signing certificate + + attr_accessor :key + + ## + # The digest algorithm used to create the signature + + attr_reader :digest_algorithm + + ## + # The name of the digest algorithm, used to pull digests out of the hash by + # name. + + attr_reader :digest_name # :nodoc: + + ## + # Creates a new signer with an RSA +key+ or path to a key, and a certificate + # +chain+ containing X509 certificates, encoding certificates or paths to + # certificates. + + def initialize key, cert_chain + @cert_chain = cert_chain + @key = key + + unless @key then + default_key = File.join Gem.user_home, 'gem-private_key.pem' + @key = default_key if File.exist? default_key + end + + unless @cert_chain then + default_cert = File.join Gem.user_home, 'gem-public_cert.pem' + @cert_chain = [default_cert] if File.exist? default_cert + end + + @digest_algorithm = Gem::Security::DIGEST_ALGORITHM + @digest_name = Gem::Security::DIGEST_NAME + + @key = OpenSSL::PKey::RSA.new File.read @key if + @key and not OpenSSL::PKey::RSA === @key + + if @cert_chain then + @cert_chain = @cert_chain.compact.map do |cert| + next cert if OpenSSL::X509::Certificate === cert + + cert = File.read cert if File.exist? cert + + OpenSSL::X509::Certificate.new cert + end + + load_cert_chain + end + end + + ## + # Loads any missing issuers in the cert chain from the trusted certificates. + # + # If the issuer does not exist it is ignored as it will be checked later. + + def load_cert_chain # :nodoc: + return if @cert_chain.empty? + + while @cert_chain.first.issuer.to_s != @cert_chain.first.subject.to_s do + issuer = Gem::Security.trust_dir.issuer_of @cert_chain.first + + break unless issuer # cert chain is verified later + + @cert_chain.unshift issuer + end + end + + ## + # Sign data with given digest algorithm + + def sign data + return unless @key + + if @cert_chain.length == 1 and @cert_chain.last.not_after < Time.now then + re_sign_key + end + + Gem::Security::SigningPolicy.verify @cert_chain, @key + + @key.sign @digest_algorithm.new, data + end + + ## + # Attempts to re-sign the private key if the signing certificate is expired. + # + # The key will be re-signed if: + # * The expired certificate is self-signed + # * The expired certificate is saved at ~/.gem/gem-public_cert.pem + # * There is no file matching the expiry date at + # ~/.gem/gem-public_cert.pem.expired.%Y%m%d%H%M%S + # + # If the signing certificate can be re-signed the expired certificate will + # be saved as ~/.gem/gem-pubilc_cert.pem.expired.%Y%m%d%H%M%S where the + # expiry time (not after) is used for the timestamp. + + def re_sign_key # :nodoc: + old_cert = @cert_chain.last + + disk_cert_path = File.join Gem.user_home, 'gem-public_cert.pem' + disk_cert = File.read disk_cert_path rescue nil + disk_key = + File.read File.join(Gem.user_home, 'gem-private_key.pem') rescue nil + + if disk_key == @key.to_pem and disk_cert == old_cert.to_pem then + expiry = old_cert.not_after.strftime '%Y%m%d%H%M%S' + old_cert_file = "gem-public_cert.pem.expired.#{expiry}" + old_cert_path = File.join Gem.user_home, old_cert_file + + unless File.exist? old_cert_path then + Gem::Security.write old_cert, old_cert_path + + cert = Gem::Security.re_sign old_cert, @key + + Gem::Security.write cert, disk_cert_path + + @cert_chain = [cert] + end + end + end + +end + diff --git a/lib/rubygems/security/trust_dir.rb b/lib/rubygems/security/trust_dir.rb new file mode 100644 index 0000000000..dd51308ee5 --- /dev/null +++ b/lib/rubygems/security/trust_dir.rb @@ -0,0 +1,104 @@ +class Gem::Security::TrustDir + + DEFAULT_PERMISSIONS = { + :trust_dir => 0700, + :trusted_cert => 0600, + } + + def initialize dir, permissions = DEFAULT_PERMISSIONS + @dir = dir + @permissions = permissions + + @digester = Gem::Security::DIGEST_ALGORITHM + end + + attr_reader :dir + + ## + # Returns the path to the trusted +certificate+ + + def cert_path certificate + name_path certificate.subject + end + + ## + # Enumerates trusted certificates. + + def each_certificate + return enum_for __method__ unless block_given? + + glob = File.join @dir, '*.pem' + + Dir[glob].each do |certificate_file| + begin + certificate = load_certificate certificate_file + + yield certificate, certificate_file + rescue OpenSSL::X509::CertificateError + next # HACK warn + end + end + end + + ## + # Returns the issuer certificate of the given +certificate+ if it exists in + # the trust directory. + + def issuer_of certificate + path = name_path certificate.issuer + + return unless File.exist? path + + load_certificate path + end + + ## + # Returns the path to the trusted certificate with the given ASN.1 +name+ + + def name_path name + digest = @digester.hexdigest name.to_s + + File.join @dir, "cert-#{digest}.pem" + end + + ## + # Loads the given +certificate_file+ + + def load_certificate certificate_file + pem = File.read certificate_file + + OpenSSL::X509::Certificate.new pem + end + + ## + # Add a certificate to trusted certificate list. + + def trust_cert certificate + verify + + destination = cert_path certificate + + open destination, 'wb', @permissions[:trusted_cert] do |io| + io.write certificate.to_pem + end + end + + ## + # Make sure the trust directory exists. If it does exist, make sure it's + # actually a directory. If not, then create it with the appropriate + # permissions. + + def verify + if File.exist? @dir then + raise Gem::Security::Exception, + "trust directory #{@dir} is not a directory" unless + File.directory? @dir + + FileUtils.chmod 0700, @dir + else + FileUtils.mkdir_p @dir, :mode => @permissions[:trust_dir] + end + end + +end + diff --git a/lib/rubygems/server.rb b/lib/rubygems/server.rb index 47fa7c562d..b640186b64 100644 --- a/lib/rubygems/server.rb +++ b/lib/rubygems/server.rb @@ -3,7 +3,7 @@ require 'zlib' require 'erb' require 'rubygems' -require 'rubygems/doc_manager' +require 'rubygems/rdoc' ## # Gem::Server and allows users to serve gems for consumption by @@ -17,9 +17,6 @@ require 'rubygems/doc_manager' # * "/quick/" - Individual gemspecs # * "/gems" - Direct access to download the installable gems # * "/rdoc?q=" - Search for installed rdoc documentation -# * legacy indexes: -# * "/Marshal.#{Gem.marshal_version}" - Full SourceIndex dump of metadata -# for installed gems # # == Usage # @@ -430,53 +427,25 @@ div.method-source-code pre { color: #ffdead; overflow: hidden; } options[:launch], options[:addresses]).run end - ## - # Only the first directory in gem_dirs is used for serving gems - def initialize(gem_dirs, port, daemon, launch = nil, addresses = nil) + Gem::RDoc.load_rdoc Socket.do_not_reverse_lookup = true - @gem_dirs = Array gem_dirs - @port = port - @daemon = daemon - @launch = launch + @gem_dirs = Array gem_dirs + @port = port + @daemon = daemon + @launch = launch @addresses = addresses - logger = WEBrick::Log.new nil, WEBrick::BasicLog::FATAL - @server = WEBrick::HTTPServer.new :DoNotListen => true, :Logger => logger - @spec_dirs = @gem_dirs.map do |gem_dir| - spec_dir = File.join gem_dir, 'specifications' - - unless File.directory? spec_dir then - raise ArgumentError, "#{gem_dir} does not appear to be a gem repository" - end + logger = WEBrick::Log.new nil, WEBrick::BasicLog::FATAL + @server = WEBrick::HTTPServer.new :DoNotListen => true, :Logger => logger - spec_dir - end + @spec_dirs = @gem_dirs.map { |gem_dir| File.join gem_dir, 'specifications' } + @spec_dirs.reject! { |spec_dir| !File.directory? spec_dir } Gem::Specification.dirs = @gem_dirs - end - def Marshal(req, res) - Gem::Specification.reset - - add_date res - - index = Gem::Deprecate.skip_during { Marshal.dump Gem.source_index } - - if req.request_method == 'HEAD' then - res['content-length'] = index.length - return - end - - if req.path =~ /Z$/ then - res['content-type'] = 'application/x-deflate' - index = Gem.deflate index - else - res['content-type'] = 'application/octet-stream' - end - - res.body << index + @have_rdoc_4_plus = nil end def add_date res @@ -485,6 +454,19 @@ div.method-source-code pre { color: #ffdead; overflow: hidden; } end.max end + def doc_root gem_name + if have_rdoc_4_plus? then + "/doc_root/#{gem_name}/" + else + "/doc_root/#{gem_name}/rdoc/index.html" + end + end + + def have_rdoc_4_plus? + @have_rdoc_4_plus ||= + Gem::Requirement.new('>= 4').satisfied_by? Gem::RDoc.rdoc_version + end + def latest_specs(req, res) Gem::Specification.reset @@ -614,14 +596,14 @@ div.method-source-code pre { color: #ffdead; overflow: hidden; } "authors" => spec.authors.sort.join(", "), "date" => spec.date.to_s, "dependencies" => deps, - "doc_path" => "/doc_root/#{spec.full_name}/rdoc/index.html", + "doc_path" => doc_root(spec.full_name), "executables" => executables, "only_one_executable" => (executables && executables.size == 1), "full_name" => spec.full_name, "has_deps" => !deps.empty?, "homepage" => spec.homepage, "name" => spec.name, - "rdoc_installed" => Gem::DocManager.new(spec).rdoc_installed?, + "rdoc_installed" => Gem::RDoc.new(spec).rdoc_installed?, "summary" => spec.summary, "version" => spec.version.to_s, } @@ -630,7 +612,7 @@ div.method-source-code pre { color: #ffdead; overflow: hidden; } specs << { "authors" => "Chad Fowler, Rich Kilmer, Jim Weirich, Eric Hodel and others", "dependencies" => [], - "doc_path" => "/doc_root/rubygems-#{Gem::VERSION}/rdoc/index.html", + "doc_path" => doc_root("rubygems-#{Gem::VERSION}"), "executables" => [{"executable" => 'gem', "is_last" => true}], "only_one_executable" => true, "full_name" => "rubygems-#{Gem::VERSION}", @@ -730,15 +712,15 @@ div.method-source-code pre { color: #ffdead; overflow: hidden; } when 1 new_path = File.basename(found_gems[0]) res.status = 302 - res['Location'] = "/doc_root/#{new_path}/rdoc/index.html" + res['Location'] = doc_root new_path return true else doc_items = [] found_gems.each do |file_name| base_name = File.basename(file_name) doc_items << { - :name => base_name, - :url => "/doc_root/#{base_name}/rdoc/index.html", + :name => base_name, + :url => doc_root(new_path), :summary => '' } end @@ -756,9 +738,6 @@ div.method-source-code pre { color: #ffdead; overflow: hidden; } WEBrick::Daemon.start if @daemon - @server.mount_proc "/Marshal.#{Gem.marshal_version}", method(:Marshal) - @server.mount_proc "/Marshal.#{Gem.marshal_version}.Z", method(:Marshal) - @server.mount_proc "/specs.#{Gem.marshal_version}", method(:specs) @server.mount_proc "/specs.#{Gem.marshal_version}.gz", method(:specs) @@ -779,10 +758,21 @@ div.method-source-code pre { color: #ffdead; overflow: hidden; } @server.mount_proc "/rdoc", method(:rdoc) - paths = { "/gems" => "/cache/", "/doc_root" => "/doc/" } - paths.each do |mount_point, mount_dir| - @server.mount(mount_point, WEBrick::HTTPServlet::FileHandler, - File.join(@gem_dirs.first, mount_dir), true) + file_handlers = { + '/gems' => '/cache/', + } + + if have_rdoc_4_plus? then + @server.mount '/doc_root', RDoc::Servlet, '/doc_root' + else + file_handlers['/doc_root'] = '/doc/' + end + + @gem_dirs.each do |gem_dir| + file_handlers.each do |mount_point, mount_dir| + @server.mount(mount_point, WEBrick::HTTPServlet::FileHandler, + File.join(gem_dir, mount_dir), true) + end end trap("INT") { @server.shutdown; exit! } diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb new file mode 100644 index 0000000000..c85a2cf660 --- /dev/null +++ b/lib/rubygems/source.rb @@ -0,0 +1,144 @@ +require 'uri' +require 'fileutils' + +class Gem::Source + FILES = { + :released => 'specs', + :latest => 'latest_specs', + :prerelease => 'prerelease_specs', + } + + def initialize(uri) + unless uri.kind_of? URI + uri = URI.parse(uri.to_s) + end + + @uri = uri + @api_uri = nil + end + + attr_reader :uri + + def api_uri + require 'rubygems/remote_fetcher' + @api_uri ||= Gem::RemoteFetcher.fetcher.api_endpoint uri + end + + def <=>(other) + if !@uri + return 0 unless other.uri + return -1 + end + + return 1 if !other.uri + + @uri.to_s <=> other.uri.to_s + end + + include Comparable + + def ==(other) + case other + when self.class + @uri == other.uri + else + false + end + end + + alias_method :eql?, :== + + def hash + @uri.hash + end + + ## + # Returns the local directory to write +uri+ to. + + def cache_dir(uri) + # Correct for windows paths + escaped_path = uri.path.sub(/^\/([a-z]):\//i, '/\\1-/') + root = File.join Gem.user_home, '.gem', 'specs' + File.join root, "#{uri.host}%#{uri.port}", File.dirname(escaped_path) + end + + def update_cache? + @update_cache ||= File.stat(Gem.user_home).uid == Process.uid + end + + def fetch_spec(name) + fetcher = Gem::RemoteFetcher.fetcher + + spec_file_name = name.spec_name + + uri = @uri + "#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}" + + cache_dir = cache_dir uri + + local_spec = File.join cache_dir, spec_file_name + + if File.exist? local_spec then + spec = Gem.read_binary local_spec + spec = Marshal.load(spec) rescue nil + return spec if spec + end + + uri.path << '.rz' + + spec = fetcher.fetch_path uri + spec = Gem.inflate spec + + if update_cache? then + FileUtils.mkdir_p cache_dir + + open local_spec, 'wb' do |io| + io.write spec + end + end + + # TODO: Investigate setting Gem::Specification#loaded_from to a URI + Marshal.load spec + end + + ## + # Loads +type+ kind of specs fetching from +@uri+ if the on-disk cache is + # out of date. + # + # +type+ is one of the following: + # + # :released => Return the list of all released specs + # :latest => Return the list of only the highest version of each gem + # :prerelease => Return the list of all prerelease only specs + # + + def load_specs(type) + file = FILES[type] + fetcher = Gem::RemoteFetcher.fetcher + file_name = "#{file}.#{Gem.marshal_version}" + spec_path = @uri + "#{file_name}.gz" + cache_dir = cache_dir spec_path + local_file = File.join(cache_dir, file_name) + retried = false + + FileUtils.mkdir_p cache_dir if update_cache? + + spec_dump = fetcher.cache_update_path(spec_path, local_file) + + begin + Gem::NameTuple.from_list Marshal.load(spec_dump) + rescue ArgumentError + if update_cache? && !retried + FileUtils.rm local_file + retried = true + retry + else + raise Gem::Exception.new("Invalid spec cache file in #{local_file}") + end + end + end + + def download(spec, dir=Dir.pwd) + fetcher = Gem::RemoteFetcher.fetcher + fetcher.download spec, @uri.to_s, dir + end +end diff --git a/lib/rubygems/source_index.rb b/lib/rubygems/source_index.rb deleted file mode 100644 index 1fe92c0a80..0000000000 --- a/lib/rubygems/source_index.rb +++ /dev/null @@ -1,406 +0,0 @@ -#-- -# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. -# All rights reserved. -# See LICENSE.txt for permissions. -#++ - -require 'rubygems/specification' -require 'rubygems/deprecate' - -## -# The SourceIndex object indexes all the gems available from a -# particular source (e.g. a list of gem directories, or a remote -# source). A SourceIndex maps a gem full name to a gem -# specification. -# -# NOTE:: The class used to be named Cache, but that became -# confusing when cached source fetchers where introduced. The -# constant Gem::Cache is an alias for this class to allow old -# YAMLized source index objects to load properly. - -class Gem::SourceIndex - - include Enumerable - - attr_reader :gems # :nodoc: - - ## - # Directories to use to refresh this SourceIndex when calling refresh! - - attr_accessor :spec_dirs - - ## - # Factory method to construct a source index instance for a given - # path. - # - # deprecated:: - # If supplied, from_installed_gems will act just like - # +from_gems_in+. This argument is deprecated and is provided - # just for backwards compatibility, and should not generally - # be used. - # - # return:: - # SourceIndex instance - - def self.from_installed_gems(*deprecated) - if deprecated.empty? - from_gems_in(*installed_spec_directories) - else - warn "NOTE: from_installed_gems(arg) is deprecated. From #{caller.first}" - from_gems_in(*deprecated) # HACK warn - end - end - - ## - # Returns a list of directories from Gem.path that contain specifications. - - def self.installed_spec_directories - # TODO: move to Gem::Utils - Gem.path.collect { |dir| File.join(dir, "specifications") } - end - - ## - # Creates a new SourceIndex from the ruby format gem specifications in - # +spec_dirs+. - - def self.from_gems_in(*spec_dirs) - new spec_dirs - end - - ## - # Loads a ruby-format specification from +file_name+ and returns the - # loaded spec. - - def self.load_specification(file_name) - Gem::Deprecate.skip_during do - Gem::Specification.load Gem::Path.new(file_name) - end - end - - ## - # Constructs a source index instance from the provided specifications, which - # is a Hash of gem full names and Gem::Specifications. - - def initialize specs_or_dirs = [] - @gems = {} - @spec_dirs = nil - - case specs_or_dirs - when Hash then - specs_or_dirs.each do |full_name, spec| - add_spec spec - end - when Array, String then - self.spec_dirs = Array(specs_or_dirs) - refresh! - else - arg = specs_or_dirs.inspect - warn "NOTE: SourceIndex.new(#{arg}) is deprecated; From #{caller.first}." - end - end - - def all_gems - gems - end - - def prerelease_gems - @gems.reject { |name, gem| !gem.version.prerelease? } - end - - def released_gems - @gems.reject { |name, gem| gem.version.prerelease? } - end - - ## - # Reconstruct the source index from the specifications in +spec_dirs+. - - def load_gems_in(*spec_dirs) - @gems.clear - - spec_dirs.reverse_each do |spec_dir| - spec_files = Dir[File.join(spec_dir, "*.gemspec")] - - spec_files.each do |spec_file| - gemspec = Gem::Deprecate.skip_during do - Gem::Specification.load spec_file - end - add_spec gemspec if gemspec - end - end - - self - end - - ## - # Returns an Array specifications for the latest released versions - # of each gem in this index. - - def latest_specs(include_prerelease=false) - result = Hash.new { |h,k| h[k] = [] } - latest = {} - - sort.each do |_, spec| - name = spec.name - curr_ver = spec.version - prev_ver = latest.key?(name) ? latest[name].version : nil - - next if !include_prerelease && curr_ver.prerelease? - next unless prev_ver.nil? or curr_ver >= prev_ver or - latest[name].platform != Gem::Platform::RUBY - - if prev_ver.nil? or - (curr_ver > prev_ver and spec.platform == Gem::Platform::RUBY) then - result[name].clear - latest[name] = spec - end - - if spec.platform != Gem::Platform::RUBY then - result[name].delete_if do |result_spec| - result_spec.platform == spec.platform - end - end - - result[name] << spec - end - - result.values.flatten - end - - ## - # An array including only the prerelease gemspecs - - def prerelease_specs - prerelease_gems.values - end - - ## - # An array including only the released gemspecs - - def released_specs - released_gems.values - end - - ## - # Add a gem specification to the source index. - - def add_spec(gem_spec, name = gem_spec.full_name) - # No idea why, but the Indexer wants to insert them using original_name - # instead of full_name. So we make it an optional arg. - @gems[name] = gem_spec - end - - ## - # Add gem specifications to the source index. - - def add_specs(*gem_specs) - Gem::Deprecate.skip_during do - gem_specs.each do |spec| - add_spec spec - end - end - end - - ## - # Remove a gem specification named +full_name+. - - def remove_spec(full_name) - @gems.delete full_name - end - - ## - # Iterate over the specifications in the source index. - - def each(&block) # :yields: gem.full_name, gem - @gems.each(&block) - end - - ## - # The gem specification given a full gem spec name. - - def specification(full_name) - @gems[full_name] - end - - ## - # The signature for the source index. Changes in the signature indicate a - # change in the index. - - def index_signature - require 'digest' - - Digest::SHA256.new.hexdigest(@gems.keys.sort.join(',')).to_s - end - - ## - # The signature for the given gem specification. - - def gem_signature(gem_full_name) - require 'digest' - - Digest::SHA256.new.hexdigest(@gems[gem_full_name].to_yaml).to_s - end - - def size - @gems.size - end - alias length size - - ## - # Find a gem by an exact match on the short name. - - def find_name(gem_name, requirement = Gem::Requirement.default) - dep = Gem::Dependency.new gem_name, requirement - - Gem::Deprecate.skip_during do - search dep - end - end - - ## - # Search for a gem by Gem::Dependency +gem_pattern+. If +only_platform+ - # is true, only gems matching Gem::Platform.local will be returned. An - # Array of matching Gem::Specification objects is returned. - # - # For backwards compatibility, a String or Regexp pattern may be passed as - # +gem_pattern+, and a Gem::Requirement for +platform_only+. This - # behavior is deprecated and will be removed. - - def search(gem_pattern, platform_or_requirement = false) - requirement = nil - only_platform = false # FIX: WTF is this?!? - - # TODO - Remove support and warning for legacy arguments after 2008/11 - unless Gem::Dependency === gem_pattern - warn "#{Gem.location_of_caller.join ':'}:Warning: Gem::SourceIndex#search support for #{gem_pattern.class} patterns is deprecated, use #find_name" - end - - case gem_pattern - when Regexp then - requirement = platform_or_requirement || Gem::Requirement.default - when Gem::Dependency then - only_platform = platform_or_requirement - requirement = gem_pattern.requirement - - gem_pattern = if Regexp === gem_pattern.name then - gem_pattern.name - elsif gem_pattern.name.empty? then - // - else - /^#{Regexp.escape gem_pattern.name}$/ - end - else - requirement = platform_or_requirement || Gem::Requirement.default - gem_pattern = /#{gem_pattern}/i - end - - unless Gem::Requirement === requirement then - requirement = Gem::Requirement.create requirement - end - - specs = @gems.values.select do |spec| - spec.name =~ gem_pattern and - requirement.satisfied_by? spec.version - end - - if only_platform then - specs = specs.select do |spec| - Gem::Platform.match spec.platform - end - end - - specs.sort_by { |s| s.sort_obj } - end - - ## - # Replaces the gems in the source index from specifications in the - # directories this source index was created from. Raises an exception if - # this source index wasn't created from a directory (via from_gems_in or - # from_installed_gems, or having spec_dirs set). - - def refresh! - raise 'source index not created from disk' if @spec_dirs.nil? - load_gems_in(*@spec_dirs) - end - - ## - # Returns an Array of Gem::Specifications that are not up to date. - - def outdated - outdateds = [] - - latest_specs.each do |local| - dependency = Gem::Dependency.new local.name, ">= #{local.version}" - - fetcher = Gem::SpecFetcher.fetcher - remotes = fetcher.find_matching dependency - remotes = remotes.map { |(_, version, _), _| version } - - latest = remotes.sort.last - - outdateds << local.name if latest and local.version < latest - end - - outdateds - end - - def ==(other) # :nodoc: - self.class === other and @gems == other.gems - end - - def dump - Marshal.dump(self) - end -end - -# :stopdoc: -module Gem - - ## - # Cache is an alias for SourceIndex to allow older YAMLized source index - # objects to load properly. - - Cache = SourceIndex - -end - -class Gem::SourceIndex - extend Gem::Deprecate - - deprecate :all_gems, :none, 2011, 10 - - deprecate :==, :none, 2011, 11 # noisy - deprecate :add_specs, :none, 2011, 11 # noisy - deprecate :each, :none, 2011, 11 - deprecate :gems, :none, 2011, 11 - deprecate :load_gems_in, :none, 2011, 11 - deprecate :refresh!, :none, 2011, 11 - deprecate :spec_dirs=, "Specification.dirs=", 2011, 11 # noisy - deprecate :add_spec, "Specification.add_spec", 2011, 11 - deprecate :find_name, "Specification.find_by_name", 2011, 11 - deprecate :gem_signature, :none, 2011, 11 - deprecate :index_signature, :none, 2011, 11 - deprecate :initialize, :none, 2011, 11 - deprecate :latest_specs, "Specification.latest_specs", 2011, 11 - deprecate :length, "Specification.all.length", 2011, 11 - deprecate :outdated, :none, 2011, 11 - deprecate :prerelease_gems, :none, 2011, 11 - deprecate :prerelease_specs, :none, 2011, 11 - deprecate :released_gems, :none, 2011, 11 - deprecate :released_specs, :none, 2011, 11 - deprecate :remove_spec, "Specification.remove_spec", 2011, 11 - deprecate :search, :none, 2011, 11 - deprecate :size, "Specification.all.size", 2011, 11 - deprecate :spec_dirs, "Specification.dirs", 2011, 11 - deprecate :specification, "Specification.find", 2011, 11 - - class << self - extend Gem::Deprecate - - deprecate :from_gems_in, :none, 2011, 10 - deprecate :from_installed_gems, :none, 2011, 10 - deprecate :installed_spec_directories, "Specification.dirs", 2011, 11 - deprecate :load_specification, :none, 2011, 10 - end -end - -# :startdoc: diff --git a/lib/rubygems/source_list.rb b/lib/rubygems/source_list.rb new file mode 100644 index 0000000000..7bd8ef0b78 --- /dev/null +++ b/lib/rubygems/source_list.rb @@ -0,0 +1,87 @@ +require 'rubygems/source' + +class Gem::SourceList + def initialize + @sources = [] + end + + attr_reader :sources + + def self.from(ary) + list = new + + if ary + ary.each do |x| + list << x + end + end + + return list + end + + def initialize_copy(other) + @sources = @sources.dup + end + + def <<(obj) + src = case obj + when URI + Gem::Source.new(obj) + when Gem::Source + obj + else + Gem::Source.new(URI.parse(obj)) + end + + @sources << src + src + end + + def replace(other) + @sources.clear + + other.each do |x| + self << x + end + + self + end + + def each + @sources.each { |s| yield s.uri.to_s } + end + + def each_source(&b) + @sources.each(&b) + end + + def ==(other) + to_a == other + end + + def to_a + @sources.map { |x| x.uri.to_s } + end + + alias_method :to_ary, :to_a + + def first + @sources.first + end + + def include?(other) + if other.kind_of? Gem::Source + @sources.include? other + else + @sources.find { |x| x.uri.to_s == other.to_s } + end + end + + def delete(uri) + if uri.kind_of? Gem::Source + @sources.delete uri + else + @sources.delete_if { |x| x.uri.to_s == uri.to_s } + end + end +end diff --git a/lib/rubygems/source_local.rb b/lib/rubygems/source_local.rb new file mode 100644 index 0000000000..44b170c4a4 --- /dev/null +++ b/lib/rubygems/source_local.rb @@ -0,0 +1,92 @@ +require 'rubygems/source' + +class Gem::Source::Local < Gem::Source + def initialize + @uri = nil + end + + def load_specs(type) + names = [] + + @specs = {} + + Dir["*.gem"].each do |file| + begin + pkg = Gem::Package.new(file) + rescue SystemCallError, Gem::Package::FormatError + # ignore + else + tup = pkg.spec.name_tuple + @specs[tup] = [File.expand_path(file), pkg] + + case type + when :released + unless pkg.spec.version.prerelease? + names << pkg.spec.name_tuple + end + when :prerelease + if pkg.spec.version.prerelease? + names << pkg.spec.name_tuple + end + when :latest + tup = pkg.spec.name_tuple + + cur = names.find { |x| x.name == tup.name } + if !cur + names << tup + elsif cur.version < tup.version + names.delete cur + names << tup + end + else + names << pkg.spec.name_tuple + end + end + end + + names + end + + def find_gem(gem_name, version=Gem::Requirement.default, + prerelease=false) + load_specs :complete + + found = [] + + @specs.each do |n, data| + if n.name == gem_name + s = data[1].spec + + if version.satisfied_by?(s.version) + if prerelease + found << s + elsif !s.version.prerelease? + found << s + end + end + end + end + + found.sort_by { |s| s.version }.last + end + + def fetch_spec(name) + load_specs :complete + + if data = @specs[name] + data.last.spec + else + raise Gem::Exception, "Unable to find spec for '#{name}'" + end + end + + def download(spec, cache_dir=nil) + load_specs :complete + + @specs.each do |name, data| + return data[0] if data[1].spec == spec + end + + raise Gem::Exception, "Unable to find file for '#{spec.full_name}'" + end +end diff --git a/lib/rubygems/source_specific_file.rb b/lib/rubygems/source_specific_file.rb new file mode 100644 index 0000000000..d296e617cc --- /dev/null +++ b/lib/rubygems/source_specific_file.rb @@ -0,0 +1,28 @@ +class Gem::Source::SpecificFile < Gem::Source + def initialize(file) + @uri = nil + @path = ::File.expand_path(file) + + @package = Gem::Package.new @path + @spec = @package.spec + @name = @spec.name_tuple + end + + attr_reader :spec + + def load_specs(*a) + [@name] + end + + def fetch_spec(name) + return @spec if name == @name + raise Gem::Exception, "Unable to find '#{name}'" + @spec + end + + def download(spec, dir=nil) + return @path if spec == @spec + raise Gem::Exception, "Unable to download '#{spec.full_name}'" + end + +end diff --git a/lib/rubygems/spec_fetcher.rb b/lib/rubygems/spec_fetcher.rb index 7302ad9ffa..531d023b2f 100644 --- a/lib/rubygems/spec_fetcher.rb +++ b/lib/rubygems/spec_fetcher.rb @@ -2,6 +2,7 @@ require 'rubygems/remote_fetcher' require 'rubygems/user_interaction' require 'rubygems/errors' require 'rubygems/text' +require 'rubygems/name_tuple' ## # SpecFetcher handles metadata updates from remote gem repositories. @@ -11,17 +12,6 @@ class Gem::SpecFetcher include Gem::UserInteraction include Gem::Text - FILES = { - :all => 'specs', - :latest => 'latest_specs', - :prerelease => 'prerelease_specs', - } - - ## - # The SpecFetcher cache dir. - - attr_reader :dir # :nodoc: - ## # Cache of latest specs @@ -48,8 +38,6 @@ class Gem::SpecFetcher end def initialize - require 'fileutils' - @dir = File.join Gem.user_home, '.gem', 'specs' @update_cache = File.stat(Gem.user_home).uid == Process.uid @@ -60,144 +48,124 @@ class Gem::SpecFetcher @caches = { :latest => @latest_specs, :prerelease => @prerelease_specs, - :all => @specs + :released => @specs, } @fetcher = Gem::RemoteFetcher.fetcher end ## - # Returns the local directory to write +uri+ to. + # + # Find and fetch gem name tuples that match +dependency+. + # + # If +matching_platform+ is false, gems for all platforms are returned. - def cache_dir(uri) - # Correct for windows paths - escaped_path = uri.path.sub(/^\/([a-z]):\//i, '/\\1-/') - File.join @dir, "#{uri.host}%#{uri.port}", File.dirname(escaped_path) - end + def search_for_dependency(dependency, matching_platform=true) + found = {} - ## - # Fetch specs matching +dependency+. If +all+ is true, all matching - # (released) versions are returned. If +matching_platform+ is - # false, all platforms are returned. If +prerelease+ is true, - # prerelease versions are included. - - def fetch_with_errors(dependency, - all = false, - matching_platform = true, - prerelease = false) - - specs_and_sources, errors = find_matching_with_errors(dependency, - all, - matching_platform, - prerelease) - - ss = specs_and_sources.map do |spec_tuple, source_uri| - [fetch_spec(spec_tuple, URI.parse(source_uri)), source_uri] + rejected_specs = {} + + if dependency.prerelease? + type = :complete + elsif dependency.latest_version? + type = :latest + else + type = :released end - return [ss, errors] - end + list, errors = available_specs(type) + list.each do |source, specs| + found[source] = specs.select do |tup| + if dependency.match?(tup) + if matching_platform and !Gem::Platform.match(tup.platform) + pm = ( + rejected_specs[dependency] ||= \ + Gem::PlatformMismatch.new(tup.name, tup.version)) + pm.add_platform tup.platform + false + else + true + end + end + end + end - def fetch(*args) - fetch_with_errors(*args).first - end + errors += rejected_specs.values - def fetch_spec(spec, source_uri) - source_uri = URI.parse source_uri if String === source_uri - spec = spec - [nil, 'ruby', ''] - spec_file_name = "#{spec.join '-'}.gemspec" + tuples = [] - uri = source_uri + "#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}" + found.each do |source, specs| + specs.each do |s| + tuples << [s, source] + end + end - cache_dir = cache_dir uri + tuples = tuples.sort_by { |x| x[0] } - local_spec = File.join cache_dir, spec_file_name + return [tuples, errors] + end - if File.exist? local_spec then - spec = Gem.read_binary local_spec - else - uri.path << '.rz' - spec = @fetcher.fetch_path uri - spec = Gem.inflate spec + ## + # Return all gem name tuples who's names match +obj+ - if @update_cache then - FileUtils.mkdir_p cache_dir + def detect(type=:complete) + tuples = [] - open local_spec, 'wb' do |io| - io.write spec + list, _ = available_specs(type) + list.each do |source, specs| + specs.each do |tup| + if yield(tup) + tuples << [tup, source] end end end - # TODO: Investigate setting Gem::Specification#loaded_from to a URI - Marshal.load spec + tuples end + ## - # Find spec names that match +dependency+. If +all+ is true, all - # matching released versions are returned. If +matching_platform+ - # is false, gems for all platforms are returned. - - def find_matching_with_errors(dependency, - all = false, - matching_platform = true, - prerelease = false) - found = {} + # Find and fetch specs that match +dependency+. + # + # If +matching_platform+ is false, gems for all platforms are returned. - rejected_specs = {} + def spec_for_dependency(dependency, matching_platform=true) + tuples, errors = search_for_dependency(dependency, matching_platform) - list(all, prerelease).each do |source_uri, specs| - found[source_uri] = specs.select do |spec_name, version, spec_platform| - if dependency.match?(spec_name, version) - if matching_platform and !Gem::Platform.match(spec_platform) - pm = (rejected_specs[dependency] ||= Gem::PlatformMismatch.new(spec_name, version)) - pm.add_platform spec_platform - false - else - true - end - end + specs = [] + tuples.each do |tup, source| + begin + spec = source.fetch_spec(tup) + rescue Gem::RemoteFetcher::FetchError => e + errors << Gem::SourceFetchProblem.new(source, e) + else + specs << [spec, source] end end - errors = rejected_specs.values - - specs_and_sources = [] - - found.each do |source_uri, specs| - uri_str = source_uri.to_s - specs_and_sources.concat(specs.map { |spec| [spec, uri_str] }) - end - - [specs_and_sources, errors] - end - - def find_matching(*args) - find_matching_with_errors(*args).first + return [specs, errors] end ## - # Suggests a gem based on the supplied +gem_name+. Returns a string - # of the gem name if an approximate match can be found or nil - # otherwise. NOTE: for performance reasons only gems which exactly - # match the first character of +gem_name+ are considered. + # Suggests gems based on the supplied +gem_name+. Returns an array of + # alternative gem names. def suggest_gems_from_name gem_name - gem_name = gem_name.downcase + gem_name = gem_name.downcase.tr('_-', '') max = gem_name.size / 2 - specs = list.values.flatten 1 + names = available_specs(:complete).first.values.flatten(1) - matches = specs.map { |name, version, platform| - next unless Gem::Platform.match platform + matches = names.map { |n| + next unless n.match_platform? - distance = levenshtein_distance gem_name, name.downcase + distance = levenshtein_distance gem_name, n.name.downcase.tr('_-', '') next if distance >= max - return [name] if distance == 0 + return [n.name] if distance == 0 - [name, distance] + [n.name, distance] }.compact matches = matches.uniq.sort_by { |name, dist| dist } @@ -206,92 +174,46 @@ class Gem::SpecFetcher end ## - # Returns a list of gems available for each source in Gem::sources. If - # +all+ is true, all released versions are returned instead of only latest - # versions. If +prerelease+ is true, include prerelease versions. - - def list(all = false, prerelease = false) - # TODO: make type the only argument - type = if all - :all - elsif prerelease - :prerelease - else - :latest - end - - list = {} - file = FILES[type] - cache = @caches[type] - - Gem.sources.each do |source_uri| - source_uri = URI.parse source_uri - - unless cache.include? source_uri - cache[source_uri] = load_specs source_uri, file - end - - list[source_uri] = cache[source_uri] - end - - if type == :all - list.values.map do |gems| - gems.reject! { |g| !g[1] || g[1].prerelease? } - end - end - - list - end - - ## - # Loads specs in +file+, fetching from +source_uri+ if the on-disk cache is - # out of date. - - def load_specs(source_uri, file) - file_name = "#{file}.#{Gem.marshal_version}" - spec_path = source_uri + "#{file_name}.gz" - cache_dir = cache_dir spec_path - local_file = File.join(cache_dir, file_name) - loaded = false - - if File.exist? local_file then + # Returns a list of gems available for each source in Gem::sources. + # + # +type+ can be one of 3 values: + # :released => Return the list of all released specs + # :complete => Return the list of all specs + # :latest => Return the list of only the highest version of each gem + # :prerelease => Return the list of all prerelease only specs + # + + def available_specs(type) + errors = [] + list = {} + + Gem.sources.each_source do |source| begin - spec_dump = - @fetcher.fetch_path(spec_path, File.mtime(local_file)) + names = case type + when :latest + tuples_for source, :latest + when :released + tuples_for source, :released + when :complete + tuples_for(source, :prerelease) + tuples_for(source, :released) + when :prerelease + tuples_for(source, :prerelease) + else + raise Gem::Exception, "Unknown type - :#{type}" + end rescue Gem::RemoteFetcher::FetchError => e - alert_warning "Error fetching data: #{e.message}" + errors << Gem::SourceFetchProblem.new(source, e) + else + list[source] = names end - - loaded = true if spec_dump - - spec_dump ||= Gem.read_binary local_file - else - spec_dump = @fetcher.fetch_path spec_path - loaded = true end - specs = begin - Marshal.load spec_dump - rescue ArgumentError - spec_dump = @fetcher.fetch_path spec_path - loaded = true - - Marshal.load spec_dump - end - - if loaded and @update_cache then - begin - FileUtils.mkdir_p cache_dir - - open local_file, 'wb' do |io| - io << spec_dump - end - rescue - end - end - - specs + [list, errors] end + def tuples_for(source, type) + cache = @caches[type] + cache[source.uri] ||= source.load_specs(type) + end end diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb index 70a3fd09b4..1d290c8af5 100644 --- a/lib/rubygems/specification.rb +++ b/lib/rubygems/specification.rb @@ -4,32 +4,56 @@ # See LICENSE.txt for permissions. #++ +## +# The Specification class contains the information for a Gem. Typically +# defined in a .gemspec file or a Rakefile, and looks like this: +# +# Gem::Specification.new do |s| +# s.name = 'example' +# s.version = '0.1.0' +# s.summary = "This is an example!" +# s.description = "Much longer explanation of the example!" +# s.authors = ["Ruby Coder"] +# s.email = 'rubycoder@example.com' +# s.files = ["lib/example.rb"] +# s.homepage = 'http://rubygems.org/gems/example' +# end +# +# Starting in RubyGems 1.9.0, a Specification can hold arbitrary +# metadata. This metadata is accessed via Specification#metadata +# and has the following restrictions: +# +# * Must be a Hash object +# * All keys and values must be Strings +# * Keys can be a maximum of 128 bytes and values can be a +# maximum of 1024 bytes +# * All strings must be UTF8, no binary data is allowed +# +# For example, to add metadata for the location of a bugtracker: +# +# s.metadata = { "bugtracker" => "http://somewhere.com/blah" } +# + + require 'rubygems/version' require 'rubygems/requirement' require 'rubygems/platform' -require "rubygems/deprecate" +require 'rubygems/deprecate' # :stopdoc: -class Date; end # for ruby_code if date.rb wasn't required +# date.rb can't be loaded for `make install` due to miniruby +# Date is needed for old gems that stored #date as Date instead of Time. +class Date; end # :startdoc: -## -# The Specification class contains the metadata for a Gem. Typically -# defined in a .gemspec file or a Rakefile, and looks like this: -# -# spec = Gem::Specification.new do |s| -# s.name = 'example' -# s.version = '1.0' -# s.summary = 'Example gem specification' -# ... -# end -# -# For a great way to package gems, use Hoe. - class Gem::Specification + # REFACTOR: Consider breaking out this version stuff into a separate + # module. There's enough special stuff around it that it may justify + # a separate class. + ## - # The the version number of a specification that does not specify one + # The version number of a specification that does not specify one # (i.e. RubyGems 0.7 or earlier). NONEXISTENT_SPECIFICATION_VERSION = -1 @@ -49,17 +73,37 @@ class Gem::Specification # 2 0.9.5 2007-10-01 Added "required_rubygems_version" # Now forward-compatible with future versions # 3 1.3.2 2009-01-03 Added Fixnum validation to specification_version + # 4 1.9.0 2011-06-07 Added metadata #-- # When updating this number, be sure to also update #to_ruby. # # NOTE RubyGems < 1.2 cannot load specification versions > 2. - CURRENT_SPECIFICATION_VERSION = 3 - - # :stopdoc: + CURRENT_SPECIFICATION_VERSION = 4 + + ## + # An informal list of changes to the specification. The highest-valued + # key should be equal to the CURRENT_SPECIFICATION_VERSION. + + SPECIFICATION_VERSION_HISTORY = { + -1 => ['(RubyGems versions up to and including 0.7 did not have versioned specifications)'], + 1 => [ + 'Deprecated "test_suite_file" in favor of the new, but equivalent, "test_files"', + '"test_file=x" is a shortcut for "test_files=[x]"' + ], + 2 => [ + 'Added "required_rubygems_version"', + 'Now forward-compatible with future versions', + ], + 3 => [ + 'Added Fixnum validation to the specification_version' + ], + 4 => [ + 'Added sandboxed freeform metadata to the specification version.' + ] + } - # version => # of fields - MARSHAL_FIELDS = { -1 => 16, 1 => 16, 2 => 16, 3 => 17 } + MARSHAL_FIELDS = { -1 => 16, 1 => 16, 2 => 16, 3 => 17, 4 => 18 } today = Time.now.utc TODAY = Time.utc(today.year, today.month, today.day) @@ -95,6 +139,7 @@ class Gem::Specification :files => [], :homepage => nil, :licenses => [], + :metadata => {}, :name => nil, :platform => Gem::Platform::RUBY, :post_install_message => nil, @@ -122,19 +167,37 @@ class Gem::Specification # :section: Required gemspec attributes ## - # This gem's name + # This gem's name. + # + # Usage: + # + # spec.name = 'rake' attr_accessor :name ## - # This gem's version + # This gem's version. + # + # The version string can contain numbers and periods, such as +1.0.0+. + # A gem is a 'prerelease' gem if the version has a letter in it, such as + # +1.0.0.pre+. + # + # Usage: + # + # spec.version = '0.4.1' attr_reader :version ## - # Paths in the gem to add to $LOAD_PATH when this gem is activated. + # Paths in the gem to add to $LOAD_PATH when this gem is activated. # - # The default ['lib'] is typically sufficient. + # Usage: + # + # # If all library files are in the root directory... + # spec.require_path = '.' + # + # # If you have 'lib' and 'ext' directories... + # spec.require_paths << 'ext' attr_accessor :require_paths @@ -146,32 +209,113 @@ class Gem::Specification attr_accessor :rubygems_version ## - # The Gem::Specification version of this gemspec. + # A short summary of this gem's description. Displayed in `gem list -d`. # - # Do not set this, it is set automatically when the gem is packaged. + # The description should be more detailed than the summary. + # + # Usage: + # + # spec.summary = "This is a small summary of my gem" - attr_accessor :specification_version + attr_reader :summary ## - # A short summary of this gem's description. Displayed in `gem list -d`. + # The platform this gem runs on. # - # The description should be more detailed than the summary. For example, - # you might wish to copy the entire README into the description. + # This is usually Gem::Platform::RUBY or Gem::Platform::CURRENT. + # + # Most gems contain pure Ruby code; they should simply leave the default value + # in place. Some gems contain C (or other) code to be compiled into a Ruby + # “extensionâ€. The should leave the default value in place unless their code + # will only compile on a certain type of system. Some gems consist of + # pre-compiled code (“binary gemsâ€). It’s especially important that they set + # the platform attribute appropriately. A shortcut is to set the platform to + # Gem::Platform::CURRENT, which will cause the gem builder to set the platform + # to the appropriate value for the system on which the build is being performed. + # + # If this attribute is set to a non-default value, it will be included in the + # filename of the gem when it is built, e.g. fxruby-1.2.0-win32.gem. + # + # Usage: + # + # spec.platform = Gem::Platform::Win32 - attr_reader :summary + def platform= platform + if @original_platform.nil? or + @original_platform == Gem::Platform::RUBY then + @original_platform = platform + end - ###################################################################### - # :section: Optional gemspec attributes + case platform + when Gem::Platform::CURRENT then + @new_platform = Gem::Platform.local + @original_platform = @new_platform.to_s + + when Gem::Platform then + @new_platform = platform + + # legacy constants + when nil, Gem::Platform::RUBY then + @new_platform = Gem::Platform::RUBY + when 'mswin32' then # was Gem::Platform::WIN32 + @new_platform = Gem::Platform.new 'x86-mswin32' + when 'i586-linux' then # was Gem::Platform::LINUX_586 + @new_platform = Gem::Platform.new 'x86-linux' + when 'powerpc-darwin' then # was Gem::Platform::DARWIN + @new_platform = Gem::Platform.new 'ppc-darwin' + else + @new_platform = Gem::Platform.new platform + end + + @platform = @new_platform.to_s + + invalidate_memoized_attributes + + @new_platform + end ## - # Autorequire was used by old RubyGems to automatically require a file. + # Files included in this gem. You cannot append to this accessor, you must + # assign to it. # - # Deprecated: It is neither supported nor functional. + # Only add files you can require to this list, not directories, etc. + # + # Directories are automatically stripped from this list when building a gem, + # other non-files cause an error. + # + # Usage: + # + # require 'rake' + # spec.files = FileList['lib/**/*.rb', + # 'bin/*', + # '[A-Z]*', + # 'test/**/*'].to_a + # + # # or without Rake... + # spec.files = Dir['lib/**/*.rb'] + Dir['bin/*'] + # spec.files += Dir['[A-Z]*'] + Dir['test/**/*'] + # spec.files.reject! { |fn| fn.include? "CVS" } - attr_accessor :autorequire + def files + # DO NOT CHANGE TO ||= ! This is not a normal accessor. (yes, it sucks) + # DOC: Why isn't it normal? Why does it suck? How can we fix this? + @files = [@files, + @test_files, + add_bindir(@executables), + @extra_rdoc_files, + @extensions, + ].flatten.uniq.compact + end + + ###################################################################### + # :section: Optional gemspec attributes ## # The path in the gem for executable scripts. Usually 'bin' + # + # Usage: + # + # spec.bindir = 'bin' attr_accessor :bindir @@ -183,39 +327,241 @@ class Gem::Specification ## # A long description of this gem + # + # The description should be more detailed than the summary. + # + # Usage: + # + # spec.description = <<-EOF + # Rake is a Make-like program implemented in Ruby. Tasks and + # dependencies are specified in standard Ruby syntax. + # EOF attr_reader :description ## - # Sets the default executable for this gem. + # A contact email for this gem # - # Deprecated: You must now specify the executable name to Gem.bin_path. + # Usage: + # + # spec.email = 'john.jones@example.com' + # spec.email = ['jack@example.com', 'jill@example.com'] - attr_writer :default_executable + attr_accessor :email ## - # A contact email for this gem + # The URL of this gem's home page # - # If you are providing multiple authors and multiple emails they should be - # in the same order such that: + # Usage: # - # Hash[*spec.authors.zip(spec.emails).flatten] + # spec.homepage = 'http://rake.rubyforge.org' + + attr_accessor :homepage + + ## + # A message that gets displayed after the gem is installed. # - # Gives a hash of author name to email address. + # Usage: + # + # spec.post_install_message = "Thanks for installing!" - attr_accessor :email + attr_accessor :post_install_message ## - # The URL of this gem's home page + # The key used to sign this gem. See Gem::Security for details. - attr_accessor :homepage + attr_accessor :signing_key ## - # True when this gemspec has been activated. This attribute is not persisted. + # :attr_accessor: metadata + # + # Arbitrary metadata for this gem. An instance of Hash. + # + # metadata is simply a Symbol => String association that contains arbitary + # data that could be useful to other consumers. + + attr_accessor :metadata + + ## + # Adds a development dependency named +gem+ with +requirements+ to this + # gem. + # + # Usage: + # + # spec.add_development_dependency 'example', '~> 1.1', '>= 1.1.4' + # + # Development dependencies aren't installed by default and aren't + # activated when a gem is required. + + def add_development_dependency(gem, *requirements) + add_dependency_with_type(gem, :development, *requirements) + end + + ## + # Adds a runtime dependency named +gem+ with +requirements+ to this gem. + # + # Usage: + # + # spec.add_runtime_dependency 'example', '~> 1.1', '>= 1.1.4' + + def add_runtime_dependency(gem, *requirements) + add_dependency_with_type(gem, :runtime, *requirements) + end + + ## + # Singular writer for #authors + # + # Usage: + # + # spec.author = 'John Jones' + + def author= o + self.authors = [o] + end + + ## + # Sets the list of authors, ensuring it is an array. + # + # Usage: + # + # spec.authors = ['John Jones', 'Mary Smith'] + + def authors= value + @authors = Array(value).flatten.grep(String) + end - attr_accessor :loaded # :nodoc: + ## + # Executables included in the gem. + # + # For example, the rake gem has rake as an executable. You don’t specify the + # full path (as in bin/rake); all application-style files are expected to be + # found in bindir. These files must be executable ruby files. Files that + # use bash or other interpreters will not work. + # + # Usage: + # + # spec.executables << 'rake' + + def executables + @executables ||= [] + end + + ## + # Extensions to build when installing the gem, specifically the paths to + # extconf.rb-style files used to compile extensions. + # + # These files will be run when the gem is installed, causing the C (or + # whatever) code to be compiled on the user’s machine. + # + # Usage: + # + # spec.extensions << 'ext/rmagic/extconf.rb' + + def extensions + @extensions ||= [] + end + + ## + # Extra files to add to RDoc such as README or doc/examples.txt + # + # When the user elects to generate the RDoc documentation for a gem (typically + # at install time), all the library files are sent to RDoc for processing. + # This option allows you to have some non-code files included for a more + # complete set of documentation. + # + # Usage: + # + # spec.extra_rdoc_files = ['README', 'doc/user-guide.txt'] + + def extra_rdoc_files + @extra_rdoc_files ||= [] + end + + ## + # The license for this gem. + # + # The license must be a short name, no more than 64 characters. + # + # This should just be the name of your license. The full + # text of the license should be inside of the gem when you build it. + # + # Usage: + # spec.license = 'MIT' + + def license=o + self.licenses = [o] + end + + ## + # The license(s) for the library. + # + # Each license must be a short name, no more than 64 characters. + # + # This should just be the name of your license. The full + # text of the license should be inside of the gem when you build it. + # + # Usage: + # spec.licenses = ['MIT', 'GPL-2'] - alias :loaded? :loaded # :nodoc: + def licenses= licenses + @licenses = Array licenses + end + + ## + # Specifies the rdoc options to be used when generating API documentation. + # + # Usage: + # + # spec.rdoc_options << '--title' << 'Rake -- Ruby Make' << + # '--main' << 'README' << + # '--line-numbers' + + def rdoc_options + @rdoc_options ||= [] + end + + ## + # The version of ruby required by this gem + # + # Usage: + # + # # If it will work with 1.8.6 or greater... + # spec.required_ruby_version = '>= 1.8.6' + # + # # Hopefully by now: + # spec.required_ruby_version = '>= 1.9.2' + + def required_ruby_version= req + @required_ruby_version = Gem::Requirement.create req + end + + ## + # Lists the external (to RubyGems) requirements that must be met for this gem + # to work. It’s simply information for the user. + # + # Usage: + # + # spec.requirements << 'libmagick, v6.0' + # spec.requirements << 'A good graphics card' + + def requirements + @requirements ||= [] + end + + ## + # A collection of unit test files. They will be loaded as unit tests when + # the user requests a gem to be unit tested. + # + # Usage: + # spec.test_files = Dir.glob('test/tc_*.rb') + # spec.test_files = ['tests/test-suite.rb'] + + def test_files= files + @test_files = Array files + end + + ###################################################################### + # :section: Specification internals ## # True when this gemspec has been activated. This attribute is not persisted. @@ -224,6 +570,20 @@ class Gem::Specification alias :activated? :activated + ## + # Autorequire was used by old RubyGems to automatically require a file. + # + # Deprecated: It is neither supported nor functional. + + attr_accessor :autorequire + + ## + # Sets the default executable for this gem. + # + # Deprecated: You must now specify the executable name to Gem.bin_path. + + attr_writer :default_executable + ## # Path this gemspec was loaded from. This attribute is not persisted. @@ -234,11 +594,6 @@ class Gem::Specification attr_writer :original_platform # :nodoc: - ## - # A message that gets displayed after the gem is installed - - attr_accessor :post_install_message - ## # The version of ruby required by this gem @@ -248,7 +603,6 @@ class Gem::Specification # The RubyGems version required by this gem attr_reader :required_rubygems_version - ## # The rubyforge project this gem lives under. i.e. RubyGems' # rubyforge_project is "rubygems". @@ -256,22 +610,49 @@ class Gem::Specification attr_accessor :rubyforge_project ## - # The key used to sign this gem. See Gem::Security for details. + # The Gem::Specification version of this gemspec. + # + # Do not set this, it is set automatically when the gem is packaged. - attr_accessor :signing_key + attr_accessor :specification_version - def self._all # :nodoc: - unless defined?(@@all) && @@all then - specs = {} + class << self + def default_specifications_dir + File.join(Gem.default_dir, "specifications", "default") + end - self.dirs.each { |dir| + private + def each_spec(search_dirs) # :nodoc: + search_dirs.each { |dir| Dir[File.join(dir, "*.gemspec")].each { |path| spec = Gem::Specification.load path.untaint # #load returns nil if the spec is bad, so we just ignore # it at this stage - specs[spec.full_name] ||= spec if spec + yield(spec) if spec } } + end + + def each_default(&block) # :nodoc: + each_spec([default_specifications_dir], + &block) + end + + def each_normal(&block) # :nodoc: + each_spec(dirs, &block) + end + end + + def self._all # :nodoc: + unless defined?(@@all) && @@all then + + specs = {} + each_default do |spec| + specs[spec.full_name] ||= spec + end + each_normal do |spec| + specs[spec.full_name] ||= spec + end @@all = specs.values @@ -288,6 +669,15 @@ class Gem::Specification } end + ## + # Loads the default specifications. It should be called only once. + + def self.load_defaults + each_default do |spec| + Gem.register_default_spec(spec) + end + end + ## # Adds +spec+ to the known specifications, keeping the collection # properly sorted. @@ -380,7 +770,7 @@ class Gem::Specification def self.dirs @@dirs ||= Gem.path.collect { |dir| - File.join dir, "specifications" + File.join dir.dup.untaint, "specifications" } end @@ -444,12 +834,22 @@ class Gem::Specification } end + ## + # Return the best specification that contains the file matching +path+ + # amongst the specs that are not activated. + + def self.find_inactive_by_path path + self.find { |spec| + spec.contains_requirable_file? path unless spec.activated? + } + end + ## # Return currently unresolved specs that contain the file matching +path+. def self.find_in_unresolved path # TODO: do we need these?? Kill it - specs = Gem.unresolved_deps.values.map { |dep| dep.to_specs }.flatten + specs = unresolved_deps.values.map { |dep| dep.to_specs }.flatten specs.find_all { |spec| spec.contains_requirable_file? path } end @@ -459,7 +859,7 @@ class Gem::Specification # specs that contain the file matching +path+. def self.find_in_unresolved_tree path - specs = Gem.unresolved_deps.values.map { |dep| dep.to_specs }.flatten + specs = unresolved_deps.values.map { |dep| dep.to_specs }.flatten specs.reverse_each do |spec| trails = [] @@ -498,13 +898,9 @@ class Gem::Specification raise Gem::Exception, "YAML data doesn't evaluate to gem specification" end - unless (spec.instance_variables.include? '@specification_version' or - spec.instance_variables.include? :@specification_version) and - spec.instance_variable_get :@specification_version - spec.instance_variable_set :@specification_version, - NONEXISTENT_SPECIFICATION_VERSION - end - + spec.instance_eval { @specification_version ||= NONEXISTENT_SPECIFICATION_VERSION } + spec.reset_nil_attributes_to_default + spec end @@ -533,9 +929,9 @@ class Gem::Specification # Loads Ruby format gemspec from +file+. def self.load file - return unless file && File.file?(file) - + return unless file file = file.dup.untaint + return unless File.file?(file) code = if defined? Encoding File.read file, :mode => 'r:UTF-8:-' @@ -595,9 +991,9 @@ class Gem::Specification latest_specs.each do |local| dependency = Gem::Dependency.new local.name, ">= #{local.version}" - remotes = fetcher.find_matching dependency - remotes = remotes.map { |(_, version, _), _| version } - latest = remotes.sort.last + remotes, _ = fetcher.search_for_dependency dependency + remotes = remotes.map { |n, _| n.version } + latest = remotes.sort.last outdateds << local.name if latest and local.version < latest end @@ -635,14 +1031,27 @@ class Gem::Specification def self.reset @@dirs = nil - # from = caller.first(10).reject { |s| s =~ /minitest/ } - # warn "" - # warn "NOTE: Specification.reset from #{from.inspect}" - Gem.pre_reset_hooks.each { |hook| hook.call } + Gem.pre_reset_hooks.each { |hook| hook.call } @@all = nil + unresolved = unresolved_deps + unless unresolved.empty? then + w = "W" + "ARN" + warn "#{w}: Unresolved specs during Gem::Specification.reset:" + unresolved.values.each do |dep| + warn " #{dep}" + end + warn "#{w}: Clearing out unresolved specs." + warn "Please report a bug if this causes problems." + unresolved.clear + end Gem.post_reset_hooks.each { |hook| hook.call } end + # DOC: This method needs documented or nodoc'd + def self.unresolved_deps + @unresolved_deps ||= Hash.new { |h, n| h[n] = Gem::Dependency.new n } + end + ## # Load custom marshal format, re-initializing defaults as needed @@ -690,6 +1099,7 @@ class Gem::Specification spec.instance_variable_set :@new_platform, array[16] spec.instance_variable_set :@platform, array[16].to_s spec.instance_variable_set :@license, array[17] + spec.instance_variable_set :@metadata, array[18] spec.instance_variable_set :@loaded, false spec.instance_variable_set :@activated, false @@ -732,7 +1142,8 @@ class Gem::Specification @homepage, true, # has_rdoc @new_platform, - @licenses + @licenses, + @metadata ] end @@ -763,6 +1174,8 @@ class Gem::Specification # resolved later, as needed. def activate_dependencies + unresolved = Gem::Specification.unresolved_deps + self.runtime_dependencies.each do |spec_dep| if loaded = Gem.loaded_specs[spec_dep.name] next if spec_dep.matches_spec? loaded @@ -780,11 +1193,11 @@ class Gem::Specification specs.first.activate else name = spec_dep.name - Gem.unresolved_deps[name] = Gem.unresolved_deps[name].merge spec_dep + unresolved[name] = unresolved[name].merge spec_dep end end - Gem.unresolved_deps.delete self.name + unresolved.delete self.name end ## @@ -810,45 +1223,21 @@ class Gem::Specification def add_dependency_with_type(dependency, type, *requirements) requirements = if requirements.empty? then - Gem::Requirement.default - else - requirements.flatten - end - - unless dependency.respond_to?(:name) && - dependency.respond_to?(:version_requirements) - - dependency = Gem::Dependency.new(dependency, requirements, type) - end - - dependencies << dependency - end - - private :add_dependency_with_type - - ## - # Adds a development dependency named +gem+ with +requirements+ to this - # Gem. For example: - # - # spec.add_development_dependency 'example', '~> 1.1', '>= 1.1.4' - # - # Development dependencies aren't installed by default and aren't - # activated when a gem is required. - - def add_development_dependency(gem, *requirements) - add_dependency_with_type(gem, :development, *requirements) - end + Gem::Requirement.default + else + requirements.flatten + end - ## - # Adds a runtime dependency named +gem+ with +requirements+ to this Gem. - # For example: - # - # spec.add_runtime_dependency 'example', '~> 1.1', '>= 1.1.4' + unless dependency.respond_to?(:name) && + dependency.respond_to?(:version_requirements) + dependency = Gem::Dependency.new(dependency, requirements, type) + end - def add_runtime_dependency(gem, *requirements) - add_dependency_with_type(gem, :runtime, *requirements) + dependencies << dependency end + private :add_dependency_with_type + alias add_dependency add_runtime_dependency ## @@ -878,34 +1267,13 @@ class Gem::Specification val = authors and val.first end - ## - # Singular writer for #authors - - def author= o - self.authors = [o] - end - ## # The list of author names who wrote this gem. - # - # If you are providing multiple authors and multiple emails they should be - # in the same order such that: - # - # Hash[*spec.authors.zip(spec.emails).flatten] - # - # Gives a hash of author name to email address. def authors @authors ||= [] end - ## - # Sets the list of authors, ensuring it is an array. - - def authors= value - @authors = Array(value).flatten.grep(String) - end - ## # Returns the full path to the base gem directory. # @@ -933,6 +1301,32 @@ class Gem::Specification File.join bin_dir, name end + ## + # Returns the build_args used to install the gem + + def build_args + if File.exists? build_info_file + File.readlines(build_info_file).map { |x| x.strip } + else + [] + end + end + + ## + # Returns the full path to the build info directory + + def build_info_dir + File.join base_dir, "build_info" + end + + ## + # Returns the full path to the file containing the build + # information generated when the gem was installed + + def build_info_file + File.join build_info_dir, "#{full_name}.info" + end + ## # Returns the full path to the cache directory containing this # spec's cached gem. @@ -948,8 +1342,6 @@ class Gem::Specification @cache_file ||= File.join cache_dir, "#{full_name}.gem" end - alias :cache_gem :cache_file - ## # Return any possible conflicts against the currently loaded specs. @@ -969,17 +1361,13 @@ class Gem::Specification # Return true if this spec can require +file+. def contains_requirable_file? file - root = full_gem_path + root = full_gem_path + suffixes = Gem.suffixes - require_paths.each do |lib| + require_paths.any? do |lib| base = "#{root}/#{lib}/#{file}" - Gem.suffixes.each do |suf| - path = "#{base}#{suf}" - return true if File.file? path - end + suffixes.any? { |suf| File.file? "#{base}#{suf}" } end - - return false end ## @@ -989,10 +1377,15 @@ class Gem::Specification @date ||= TODAY end + DateTimeFormat = /\A + (\d{4})-(\d{2})-(\d{2}) + (\s+ \d{2}:\d{2}:\d{2}\.\d+ \s* (Z | [-+]\d\d:\d\d) )? + \Z/x + ## # The date this gem was created # - # Do not set this, it is set automatically when the gem is packaged. + # DO NOT set this, it is set automatically when the gem is packaged. def date= date # We want to end up with a Time object with one-day resolution. @@ -1000,7 +1393,7 @@ class Gem::Specification # way to do it. @date = case date when String then - if /\A(\d{4})-(\d{2})-(\d{2})\Z/ =~ date then + if DateTimeFormat =~ date then Time.utc($1.to_i, $2.to_i, $3.to_i) # Workaround for where the date format output from psych isn't @@ -1060,6 +1453,7 @@ class Gem::Specification # [depending_gem, dependency, [list_of_gems_that_satisfy_dependency]] def dependent_gems + # REFACTOR: out = []; each; out; ? Really? No #collect love? out = [] Gem::Specification.each do |spec| spec.dependencies.each do |dep| @@ -1097,10 +1491,21 @@ class Gem::Specification end ## - # Returns the full path to this spec's documentation directory. + # Returns the full path to this spec's documentation directory. If +type+ + # is given it will be appended to the end. For examlpe: + # + # spec.doc_dir # => "/path/to/gem_repo/doc/a-1" + # + # spec.doc_dir 'ri' # => "/path/to/gem_repo/doc/a-1/ri" - def doc_dir + def doc_dir type = nil @doc_dir ||= File.join base_dir, 'doc', full_name + + if type then + File.join @doc_dir, type + else + @doc_dir + end end def encode_with coder # :nodoc: @@ -1142,13 +1547,6 @@ class Gem::Specification self.executables = [o] end - ## - # Executables included in the gem. - - def executables - @executables ||= [] - end - ## # Sets executables to +value+, ensuring it is an array. Don't # use this, push onto the array instead. @@ -1158,14 +1556,6 @@ class Gem::Specification @executables = Array(value) end - ## - # Extensions to build when installing the gem. See - # Gem::Installer#build_extensions for valid values. - - def extensions - @extensions ||= [] - end - ## # Sets extensions to +extensions+, ensuring it is an array. Don't # use this, push onto the array instead. @@ -1175,13 +1565,6 @@ class Gem::Specification @extensions = Array extensions end - ## - # Extra files to add to RDoc such as README or doc/examples.txt - - def extra_rdoc_files - @extra_rdoc_files ||= [] - end - ## # Sets extra_rdoc_files to +files+, ensuring it is an array. Don't # use this, push onto the array instead. @@ -1200,25 +1583,6 @@ class Gem::Specification "#{full_name}.gem" end - ## - # Files included in this gem. You cannot append to this accessor, you must - # assign to it. - # - # Only add files you can require to this list, not directories, etc. - # - # Directories are automatically stripped from this list when building a gem, - # other non-files cause an error. - - def files - # DO NOT CHANGE TO ||= ! This is not a normal accessor. (yes, it sucks) - @files = [@files, - @test_files, - add_bindir(@executables), - @extra_rdoc_files, - @extensions, - ].flatten.uniq.compact - end - ## # Sets files to +files+, ensuring it is an array. @@ -1253,11 +1617,14 @@ class Gem::Specification # The full path to the gem (install path + full name). def full_gem_path - # TODO: try to get rid of this... or the awkward + # TODO: This is a heavily used method by gems, so we'll need + # to aleast just alias it to #gem_dir rather than remove it. + # TODO: also, shouldn't it default to full_name if it hasn't been written? return @full_gem_path if defined?(@full_gem_path) && @full_gem_path @full_gem_path = File.expand_path File.join(gems_dir, full_name) + @full_gem_path.untaint return @full_gem_path if File.directory? @full_gem_path @@ -1270,11 +1637,11 @@ class Gem::Specification # default Ruby platform. def full_name - if platform == Gem::Platform::RUBY or platform.nil? then - "#{@name}-#{@version}" - else - "#{@name}-#{@version}-#{platform}" - end + @full_name ||= if platform == Gem::Platform::RUBY or platform.nil? then + "#{@name}-#{@version}".untaint + else + "#{@name}-#{@version}-#{platform}".untaint + end end ## @@ -1372,13 +1739,9 @@ class Gem::Specification # Duplicates array_attributes from +other_spec+ so state isn't shared. def initialize_copy other_spec - other_ivars = other_spec.instance_variables - other_ivars = other_ivars.map { |ivar| ivar.intern } if # for 1.9 - String === other_ivars.first - self.class.array_attributes.each do |name| name = :"@#{name}" - next unless other_ivars.include? name + next unless other_spec.instance_variable_defined? name begin val = other_spec.instance_variable_get(name) @@ -1398,11 +1761,22 @@ class Gem::Specification end ## - # The directory that this gem was installed into. - # TODO: rename - horrible. this is the base_dir for a gem path + # Expire memoized instance variables that can incorrectly generate, replace + # or miss files due changes in certain attributes used to compute them. + + def invalidate_memoized_attributes + @full_name = nil + @cache_file = nil + end + + private :invalidate_memoized_attributes - def installation_path - loaded_from && base_dir + def inspect + if $DEBUG + super + else + "#<#{self.class}:0x#{__id__.to_s(16)} #{full_name}>" + end end ## @@ -1425,7 +1799,7 @@ class Gem::Specification def lib_files @files.select do |file| require_paths.any? do |path| - file.index(path) == 0 + file.start_with? path end end end @@ -1438,33 +1812,31 @@ class Gem::Specification end ## - # Singular accessor for #licenses - - def license=o - self.licenses = [o] - end - - ## - # The license(s) for the library. Each license must be a short name, no - # more than 64 characters. + # Plural accessor for setting licenses def licenses @licenses ||= [] end - ## - # Set licenses to +licenses+, ensuring it is an array. - - def licenses= licenses - @licenses = Array licenses - end - ## # Set the location a Specification was loaded from. +obj+ is converted # to a String. def loaded_from= path - @loaded_from = path.to_s + @loaded_from = path.to_s + + # reset everything @loaded_from depends upon + @base_dir = nil + @bin_dir = nil + @cache_dir = nil + @cache_file = nil + @doc_dir = nil + @full_gem_path = nil + @gem_dir = nil + @gems_dir = nil + @ri_dir = nil + @spec_dir = nil + @spec_file = nil end ## @@ -1516,6 +1888,13 @@ class Gem::Specification @extra_rdoc_files = @extra_rdoc_files.uniq if @extra_rdoc_files end + ## + # Return a NameTuple that represents this Specification + + def name_tuple + Gem::NameTuple.new name, version, original_platform + end + ## # Returns the full name (name-version) of this gemspec using the original # platform. For use with legacy gems. @@ -1542,44 +1921,6 @@ class Gem::Specification @new_platform ||= Gem::Platform::RUBY end - ## - # The platform this gem runs on. See Gem::Platform for details. - # - # Setting this to any value other than Gem::Platform::RUBY or - # Gem::Platform::CURRENT is probably wrong. - - def platform= platform - if @original_platform.nil? or - @original_platform == Gem::Platform::RUBY then - @original_platform = platform - end - - case platform - when Gem::Platform::CURRENT then - @new_platform = Gem::Platform.local - @original_platform = @new_platform.to_s - - when Gem::Platform then - @new_platform = platform - - # legacy constants - when nil, Gem::Platform::RUBY then - @new_platform = Gem::Platform::RUBY - when 'mswin32' then # was Gem::Platform::WIN32 - @new_platform = Gem::Platform.new 'x86-mswin32' - when 'i586-linux' then # was Gem::Platform::LINUX_586 - @new_platform = Gem::Platform.new 'x86-linux' - when 'powerpc-darwin' then # was Gem::Platform::DARWIN - @new_platform = Gem::Platform.new 'ppc-darwin' - else - @new_platform = Gem::Platform.new platform - end - - @platform = @new_platform.to_s - - @new_platform - end - def pretty_print(q) # :nodoc: q.group 2, 'Gem::Specification.new do |s|', 'end' do q.breakable @@ -1638,13 +1979,6 @@ class Gem::Specification end end - ## - # An ARGV style array of options to RDoc - - def rdoc_options - @rdoc_options ||= [] - end - ## # Sets rdoc_options to +value+, ensuring it is an array. Don't # use this, push onto the array instead. @@ -1668,13 +2002,6 @@ class Gem::Specification self.require_paths = [path] end - ## - # The version of ruby required by this gem - - def required_ruby_version= req - @required_ruby_version = Gem::Requirement.create req - end - ## # The RubyGems version required by this gem @@ -1682,14 +2009,6 @@ class Gem::Specification @required_rubygems_version = Gem::Requirement.create req end - ## - # An array or things required by this gem. Not used by anything - # presently. - - def requirements - @requirements ||= [] - end - ## # Set requirements to +req+, ensuring it is an array. Don't # use this, push onto the array instead. @@ -1714,6 +2033,9 @@ class Gem::Specification case obj when String then obj.dump when Array then '[' + obj.map { |x| ruby_code x }.join(", ") + ']' + when Hash then + seg = obj.keys.sort.map { |k| "#{k.to_s.dump} => #{obj[k].to_s.dump}" } + "{ #{seg.join(', ')} }" when Gem::Version then obj.to_s.dump when Date then obj.strftime('%Y-%m-%d').dump when Time then obj.strftime('%Y-%m-%d').dump @@ -1800,7 +2122,7 @@ class Gem::Specification end ## - # Singular accessor for #test_files + # Singular mutator for #test_files def test_file= file self.test_files = [file] @@ -1825,28 +2147,14 @@ class Gem::Specification end end - ## - # Set test_files to +files+, ensuring it is an array. - - def test_files= files - @test_files = Array files - end - - def test_suite_file # :nodoc: - # TODO: deprecate - test_files.first - end - - def test_suite_file= file # :nodoc: - # TODO: deprecate - @test_files = [] unless defined? @test_files - @test_files << file - end - ## # Returns a Ruby code representation of this specification, such that it can # be eval'ed and reconstruct the same specification later. Attributes that # still have their default values are omitted. + # + # REFACTOR: This, plus stuff like #ruby_code and #pretty_print, should + # probably be extracted out into some sort of separate class. SRP, do you + # speak it!??! def to_ruby mark_version @@ -1863,6 +2171,10 @@ class Gem::Specification result << "" result << " s.required_rubygems_version = #{ruby_code required_rubygems_version} if s.respond_to? :required_rubygems_version=" + if metadata and !metadata.empty? + result << " s.metadata = #{ruby_code metadata} if s.respond_to? :metadata=" + end + handled = [ :dependencies, :name, @@ -1872,6 +2184,7 @@ class Gem::Specification :version, :has_rdoc, :default_executable, + :metadata ] @@attributes.each do |attr_name| @@ -1883,34 +2196,36 @@ class Gem::Specification end end - result << nil - result << " if s.respond_to? :specification_version then" - result << " s.specification_version = #{specification_version}" - result << nil + unless dependencies.empty? then + result << nil + result << " if s.respond_to? :specification_version then" + result << " s.specification_version = #{specification_version}" + result << nil - result << " if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then" + result << " if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then" - dependencies.each do |dep| - req = dep.requirements_list.inspect - dep.instance_variable_set :@type, :runtime if dep.type.nil? # HACK - result << " s.add_#{dep.type}_dependency(%q<#{dep.name}>, #{req})" - end + dependencies.each do |dep| + req = dep.requirements_list.inspect + dep.instance_variable_set :@type, :runtime if dep.type.nil? # HACK + result << " s.add_#{dep.type}_dependency(%q<#{dep.name}>, #{req})" + end - result << " else" + result << " else" - dependencies.each do |dep| - version_reqs_param = dep.requirements_list.inspect - result << " s.add_dependency(%q<#{dep.name}>, #{version_reqs_param})" - end + dependencies.each do |dep| + version_reqs_param = dep.requirements_list.inspect + result << " s.add_dependency(%q<#{dep.name}>, #{version_reqs_param})" + end - result << ' end' + result << ' end' - result << " else" + result << " else" dependencies.each do |dep| version_reqs_param = dep.requirements_list.inspect result << " s.add_dependency(%q<#{dep.name}>, #{version_reqs_param})" end - result << " end" + result << " end" + end result << "end" result << nil @@ -1946,6 +2261,7 @@ class Gem::Specification ast = builder.tree io = StringIO.new + io.set_encoding Encoding::UTF_8 if Object.const_defined? :Encoding Psych::Visitors::Emitter.new(io).accept(ast) @@ -2057,12 +2373,42 @@ class Gem::Specification end end + # FIX: uhhhh single element array.each? [:authors].each do |field| val = self.send field raise Gem::InvalidSpecificationException, "#{field} may not be empty" if val.empty? end + unless Hash === metadata + raise Gem::InvalidSpecificationException, + 'metadata must be a hash' + end + + metadata.keys.each do |k| + if !k.kind_of?(String) + raise Gem::InvalidSpecificationException, + 'metadata keys must be a String' + end + + if k.size > 128 + raise Gem::InvalidSpecificationException, + "metadata key too large (#{k.size} > 128)" + end + end + + metadata.values.each do |k| + if !k.kind_of?(String) + raise Gem::InvalidSpecificationException, + 'metadata values must be a String' + end + + if k.size > 1024 + raise Gem::InvalidSpecificationException, + "metadata value too large (#{k.size} > 1024)" + end + end + licenses.each { |license| if license.length > 64 raise Gem::InvalidSpecificationException, @@ -2070,8 +2416,13 @@ class Gem::Specification end } + alert_warning 'licenses is empty' if licenses.empty? + + validate_permissions + # reject lazy developers: + # FIX: Doesn't this just evaluate to "FIXME" or "TODO"? lazy = '"FIxxxXME" or "TOxxxDO"'.gsub(/xxx/, '') unless authors.grep(/FI XME|TO DO/x).empty? then @@ -2117,9 +2468,34 @@ class Gem::Specification alert_warning "#{executable_path} is missing #! line" unless shebang end + dependencies.each do |dep| + prerelease_dep = dep.requirements_list.any? do |req| + Gem::Requirement.new(req).prerelease? + end + + alert_warning "prerelease dependency on #{dep} is not recommended" if + prerelease_dep + end + true end + ## + # Checks to see if the files to be packaged are world-readable. + + def validate_permissions + files.each do |file| + next if File.stat(file).world_readable? + alert_warning "#{file} is not world-readable" + end + + executables.each do |name| + exec = File.join @bindir, name + next if File.stat(exec).executable? + alert_warning "#{exec} is not executable" + end + end + ## # Set the version to +version+, potentially also setting # required_rubygems_version if +version+ indicates it is a @@ -2128,6 +2504,8 @@ class Gem::Specification def version= version @version = Gem::Version.create(version) self.required_rubygems_version = '> 1.3.1' if @version.prerelease? + invalidate_memoized_attributes + return @version end @@ -2147,15 +2525,33 @@ class Gem::Specification self.platform = Gem::Platform.new @platform end + ## + # Reset nil attributes to their default values to make the spec valid + + def reset_nil_attributes_to_default + nil_attributes = self.class.non_nil_attributes.find_all do |name| + !instance_variable_defined?("@#{name}") || instance_variable_get("@#{name}").nil? + end + + nil_attributes.each do |attribute| + default = self.default_value attribute + + value = case default + when Time, Numeric, Symbol, true, false, nil then default + else default.dup + end + + instance_variable_set "@#{attribute}", value + end + end + + def default_gem? + loaded_from && + File.dirname(loaded_from) == self.class.default_specifications_dir + end + extend Gem::Deprecate - deprecate :test_suite_file, :test_file, 2011, 10 - deprecate :test_suite_file=, :test_file=, 2011, 10 - deprecate :loaded, :activated, 2011, 10 - deprecate :loaded?, :activated?, 2011, 10 - deprecate :loaded=, :activated=, 2011, 10 - deprecate :installation_path, :base_dir, 2011, 10 - deprecate :cache_gem, :cache_file, 2011, 10 # TODO: # deprecate :has_rdoc, :none, 2011, 10 # deprecate :has_rdoc?, :none, 2011, 10 @@ -2167,5 +2563,5 @@ class Gem::Specification # deprecate :full_gem_path, :cache_file, 2011, 10 end +# DOC: What is this and why is it here, randomly, at the end of this file? Gem.clear_paths - diff --git a/lib/rubygems/ssl_certs/AddTrustExternalCARoot.pem b/lib/rubygems/ssl_certs/AddTrustExternalCARoot.pem new file mode 100644 index 0000000000..580158f50c --- /dev/null +++ b/lib/rubygems/ssl_certs/AddTrustExternalCARoot.pem @@ -0,0 +1,90 @@ +This CA certificate is for verifying HTTPS connection to; + - https://rubygems.org/ (obtained by RubyGems team) + +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=SE, O=AddTrust AB, OU=AddTrust External TTP Network, CN=AddTrust External CA Root + Validity + Not Before: May 30 10:48:38 2000 GMT + Not After : May 30 10:48:38 2020 GMT + Subject: C=SE, O=AddTrust AB, OU=AddTrust External TTP Network, CN=AddTrust External CA Root + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:b7:f7:1a:33:e6:f2:00:04:2d:39:e0:4e:5b:ed: + 1f:bc:6c:0f:cd:b5:fa:23:b6:ce:de:9b:11:33:97: + a4:29:4c:7d:93:9f:bd:4a:bc:93:ed:03:1a:e3:8f: + cf:e5:6d:50:5a:d6:97:29:94:5a:80:b0:49:7a:db: + 2e:95:fd:b8:ca:bf:37:38:2d:1e:3e:91:41:ad:70: + 56:c7:f0:4f:3f:e8:32:9e:74:ca:c8:90:54:e9:c6: + 5f:0f:78:9d:9a:40:3c:0e:ac:61:aa:5e:14:8f:9e: + 87:a1:6a:50:dc:d7:9a:4e:af:05:b3:a6:71:94:9c: + 71:b3:50:60:0a:c7:13:9d:38:07:86:02:a8:e9:a8: + 69:26:18:90:ab:4c:b0:4f:23:ab:3a:4f:84:d8:df: + ce:9f:e1:69:6f:bb:d7:42:d7:6b:44:e4:c7:ad:ee: + 6d:41:5f:72:5a:71:08:37:b3:79:65:a4:59:a0:94: + 37:f7:00:2f:0d:c2:92:72:da:d0:38:72:db:14:a8: + 45:c4:5d:2a:7d:b7:b4:d6:c4:ee:ac:cd:13:44:b7: + c9:2b:dd:43:00:25:fa:61:b9:69:6a:58:23:11:b7: + a7:33:8f:56:75:59:f5:cd:29:d7:46:b7:0a:2b:65: + b6:d3:42:6f:15:b2:b8:7b:fb:ef:e9:5d:53:d5:34: + 5a:27 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + AD:BD:98:7A:34:B4:26:F7:FA:C4:26:54:EF:03:BD:E0:24:CB:54:1A + X509v3 Key Usage: + Certificate Sign, CRL Sign + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Authority Key Identifier: + keyid:AD:BD:98:7A:34:B4:26:F7:FA:C4:26:54:EF:03:BD:E0:24:CB:54:1A + DirName:/C=SE/O=AddTrust AB/OU=AddTrust External TTP Network/CN=AddTrust External CA Root + serial:01 + + Signature Algorithm: sha1WithRSAEncryption + b0:9b:e0:85:25:c2:d6:23:e2:0f:96:06:92:9d:41:98:9c:d9: + 84:79:81:d9:1e:5b:14:07:23:36:65:8f:b0:d8:77:bb:ac:41: + 6c:47:60:83:51:b0:f9:32:3d:e7:fc:f6:26:13:c7:80:16:a5: + bf:5a:fc:87:cf:78:79:89:21:9a:e2:4c:07:0a:86:35:bc:f2: + de:51:c4:d2:96:b7:dc:7e:4e:ee:70:fd:1c:39:eb:0c:02:51: + 14:2d:8e:bd:16:e0:c1:df:46:75:e7:24:ad:ec:f4:42:b4:85: + 93:70:10:67:ba:9d:06:35:4a:18:d3:2b:7a:cc:51:42:a1:7a: + 63:d1:e6:bb:a1:c5:2b:c2:36:be:13:0d:e6:bd:63:7e:79:7b: + a7:09:0d:40:ab:6a:dd:8f:8a:c3:f6:f6:8c:1a:42:05:51:d4: + 45:f5:9f:a7:62:21:68:15:20:43:3c:99:e7:7c:bd:24:d8:a9: + 91:17:73:88:3f:56:1b:31:38:18:b4:71:0f:9a:cd:c8:0e:9e: + 8e:2e:1b:e1:8c:98:83:cb:1f:31:f1:44:4c:c6:04:73:49:76: + 60:0f:c7:f8:bd:17:80:6b:2e:e9:cc:4c:0e:5a:9a:79:0f:20: + 0a:2e:d5:9e:63:26:1e:55:92:94:d8:82:17:5a:7b:d0:bc:c7: + 8f:4e:86:04 + +-----BEGIN CERTIFICATE----- +MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU +MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs +IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 +MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h +bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v +dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt +H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 +uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX +mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX +a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN +E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 +WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD +VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 +Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU +cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx +IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN +AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH +YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 +6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC +Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX +c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a +mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= +-----END CERTIFICATE----- diff --git a/lib/rubygems/ssl_certs/Entrust_net-Secure-Server-Certification-Authority.pem b/lib/rubygems/ssl_certs/Entrust_net-Secure-Server-Certification-Authority.pem new file mode 100644 index 0000000000..b48d9cd70b --- /dev/null +++ b/lib/rubygems/ssl_certs/Entrust_net-Secure-Server-Certification-Authority.pem @@ -0,0 +1,90 @@ +This CA certificate is for verifying HTTPS connection to; + - https://d2chzxaqi4y7f8.cloudfront.net/ (prepared by AWS) + +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 927650371 (0x374ad243) + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=US, O=Entrust.net, OU=www.entrust.net/CPS incorp. by ref. (limits liab.), OU=(c) 1999 Entrust.net Limited, CN=Entrust.net Secure Server Certification Authority + Validity + Not Before: May 25 16:09:40 1999 GMT + Not After : May 25 16:39:40 2019 GMT + Subject: C=US, O=Entrust.net, OU=www.entrust.net/CPS incorp. by ref. (limits liab.), OU=(c) 1999 Entrust.net Limited, CN=Entrust.net Secure Server Certification Authority + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (1024 bit) + Modulus: + 00:cd:28:83:34:54:1b:89:f3:0f:af:37:91:31:ff: + af:31:60:c9:a8:e8:b2:10:68:ed:9f:e7:93:36:f1: + 0a:64:bb:47:f5:04:17:3f:23:47:4d:c5:27:19:81: + 26:0c:54:72:0d:88:2d:d9:1f:9a:12:9f:bc:b3:71: + d3:80:19:3f:47:66:7b:8c:35:28:d2:b9:0a:df:24: + da:9c:d6:50:79:81:7a:5a:d3:37:f7:c2:4a:d8:29: + 92:26:64:d1:e4:98:6c:3a:00:8a:f5:34:9b:65:f8: + ed:e3:10:ff:fd:b8:49:58:dc:a0:de:82:39:6b:81: + b1:16:19:61:b9:54:b6:e6:43 + Exponent: 3 (0x3) + X509v3 extensions: + Netscape Cert Type: + SSL CA, S/MIME CA, Object Signing CA + X509v3 CRL Distribution Points: + + Full Name: + DirName: C = US, O = Entrust.net, OU = www.entrust.net/CPS incorp. by ref. (limits liab.), OU = (c) 1999 Entrust.net Limited, CN = Entrust.net Secure Server Certification Authority, CN = CRL1 + + Full Name: + URI:http://www.entrust.net/CRL/net1.crl + + X509v3 Private Key Usage Period: + Not Before: May 25 16:09:40 1999 GMT, Not After: May 25 16:09:40 2019 GMT + X509v3 Key Usage: + Certificate Sign, CRL Sign + X509v3 Authority Key Identifier: + keyid:F0:17:62:13:55:3D:B3:FF:0A:00:6B:FB:50:84:97:F3:ED:62:D0:1A + + X509v3 Subject Key Identifier: + F0:17:62:13:55:3D:B3:FF:0A:00:6B:FB:50:84:97:F3:ED:62:D0:1A + X509v3 Basic Constraints: + CA:TRUE + 1.2.840.113533.7.65.0: + 0 +..V4.0.... + Signature Algorithm: sha1WithRSAEncryption + 90:dc:30:02:fa:64:74:c2:a7:0a:a5:7c:21:8d:34:17:a8:fb: + 47:0e:ff:25:7c:8d:13:0a:fb:e4:98:b5:ef:8c:f8:c5:10:0d: + f7:92:be:f1:c3:d5:d5:95:6a:04:bb:2c:ce:26:36:65:c8:31: + c6:e7:ee:3f:e3:57:75:84:7a:11:ef:46:4f:18:f4:d3:98:bb: + a8:87:32:ba:72:f6:3c:e2:3d:9f:d7:1d:d9:c3:60:43:8c:58: + 0e:22:96:2f:62:a3:2c:1f:ba:ad:05:ef:ab:32:78:87:a0:54: + 73:19:b5:5c:05:f9:52:3e:6d:2d:45:0b:f7:0a:93:ea:ed:06: + f9:b2 + +-----BEGIN CERTIFICATE----- +MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMC +VVMxFDASBgNVBAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5u +ZXQvQ1BTIGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMc +KGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDE6MDgGA1UEAxMxRW50cnVzdC5u +ZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw05OTA1 +MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIGA1UE +ChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5j +b3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF +bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUg +U2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUA +A4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQaO2f55M28Qpku0f1BBc/ +I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5gXpa0zf3 +wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OC +AdcwggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHb +oIHYpIHVMIHSMQswCQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5 +BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1p +dHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1pdGVk +MTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRp +b24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu +dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0 +MFqBDzIwMTkwNTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8Bdi +E1U9s/8KAGv7UISX8+1i0BowHQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAa +MAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EABAwwChsEVjQuMAMCBJAwDQYJKoZI +hvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyNEwr75Ji174z4xRAN +95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9n9cd +2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI= +-----END CERTIFICATE----- diff --git a/lib/rubygems/ssl_certs/VerisignClass3PublicPrimaryCertificationAuthority-G2.pem b/lib/rubygems/ssl_certs/VerisignClass3PublicPrimaryCertificationAuthority-G2.pem new file mode 100644 index 0000000000..43bad3ebdf --- /dev/null +++ b/lib/rubygems/ssl_certs/VerisignClass3PublicPrimaryCertificationAuthority-G2.pem @@ -0,0 +1,57 @@ +This CA certificate is for verifying HTTPS connection to; + - https://s3.amazon.com/ (prepared by AWS) + +Certificate: + Data: + Version: 1 (0x0) + Serial Number: + 7d:d9:fe:07:cf:a8:1e:b7:10:79:67:fb:a7:89:34:c6 + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=US, O=VeriSign, Inc., OU=Class 3 Public Primary Certification Authority - G2, OU=(c) 1998 VeriSign, Inc. - For authorized use only, OU=VeriSign Trust Network + Validity + Not Before: May 18 00:00:00 1998 GMT + Not After : Aug 1 23:59:59 2028 GMT + Subject: C=US, O=VeriSign, Inc., OU=Class 3 Public Primary Certification Authority - G2, OU=(c) 1998 VeriSign, Inc. - For authorized use only, OU=VeriSign Trust Network + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (1024 bit) + Modulus: + 00:cc:5e:d1:11:5d:5c:69:d0:ab:d3:b9:6a:4c:99: + 1f:59:98:30:8e:16:85:20:46:6d:47:3f:d4:85:20: + 84:e1:6d:b3:f8:a4:ed:0c:f1:17:0f:3b:f9:a7:f9: + 25:d7:c1:cf:84:63:f2:7c:63:cf:a2:47:f2:c6:5b: + 33:8e:64:40:04:68:c1:80:b9:64:1c:45:77:c7:d8: + 6e:f5:95:29:3c:50:e8:34:d7:78:1f:a8:ba:6d:43: + 91:95:8f:45:57:5e:7e:c5:fb:ca:a4:04:eb:ea:97: + 37:54:30:6f:bb:01:47:32:33:cd:dc:57:9b:64:69: + 61:f8:9b:1d:1c:89:4f:5c:67 + Exponent: 65537 (0x10001) + Signature Algorithm: sha1WithRSAEncryption + 51:4d:cd:be:5c:cb:98:19:9c:15:b2:01:39:78:2e:4d:0f:67: + 70:70:99:c6:10:5a:94:a4:53:4d:54:6d:2b:af:0d:5d:40:8b: + 64:d3:d7:ee:de:56:61:92:5f:a6:c4:1d:10:61:36:d3:2c:27: + 3c:e8:29:09:b9:11:64:74:cc:b5:73:9f:1c:48:a9:bc:61:01: + ee:e2:17:a6:0c:e3:40:08:3b:0e:e7:eb:44:73:2a:9a:f1:69: + 92:ef:71:14:c3:39:ac:71:a7:91:09:6f:e4:71:06:b3:ba:59: + 57:26:79:00:f6:f8:0d:a2:33:30:28:d4:aa:58:a0:9d:9d:69: + 91:fd + +-----BEGIN CERTIFICATE----- +MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJ +BgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xh +c3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcy +MTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3Jp +emVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMB4X +DTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVTMRcw +FQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMg +UHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEo +YykgMTk5OCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5 +MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCOFoUgRm1HP9SFIIThbbP4 +pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71lSk8UOg0 +13gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwID +AQABMA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSk +U01UbSuvDV1Ai2TT1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7i +F6YM40AIOw7n60RzKprxaZLvcRTDOaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpY +oJ2daZH9 +-----END CERTIFICATE----- diff --git a/lib/rubygems/syck_hack.rb b/lib/rubygems/syck_hack.rb index 9c6f4a2254..5356aa081e 100644 --- a/lib/rubygems/syck_hack.rb +++ b/lib/rubygems/syck_hack.rb @@ -46,6 +46,8 @@ module YAML end end end + + SyntaxError = Error unless defined? SyntaxError end # Sometime in the 1.9 dev cycle, the Syck constant was moved from under YAML diff --git a/lib/rubygems/test_case.rb b/lib/rubygems/test_case.rb index 38e09ccd02..ace0177e19 100644 --- a/lib/rubygems/test_case.rb +++ b/lib/rubygems/test_case.rb @@ -1,4 +1,4 @@ -at_exit { $SAFE = 1 } +# TODO: $SAFE = 1 if defined? Gem::QuickLoader Gem::QuickLoader.load_full_rubygems_library @@ -11,7 +11,14 @@ begin rescue Gem::LoadError end -require "rubygems/deprecate" +# We have to load these up front because otherwise we'll try to load +# them while we're testing rubygems, and thus we can't actually load them. +unless Gem::Dependency.new('rdoc', '>= 3.10').matching_specs.empty? + gem 'rdoc' + gem 'json' +end + +require 'rubygems/deprecate' require 'minitest/autorun' require 'fileutils' require 'tmpdir' @@ -35,16 +42,6 @@ module Gem @searcher = searcher end - ## - # Allows setting the default SourceIndex. This method is available when - # requiring 'rubygems/test_case' - - def self.source_index=(si) - raise "This method is not supported" - Gem::Specification.reset if si # HACK - @@source_index = si - end - ## # Allows toggling Windows behavior. This method is available when requiring # 'rubygems/test_case' @@ -97,7 +94,9 @@ class Gem::TestCase < MiniTest::Unit::TestCase undef_method :default_test if instance_methods.include? 'default_test' or instance_methods.include? :default_test - @@project_dir = Dir.pwd unless defined?(@@project_dir) + @@project_dir = Dir.pwd.untaint unless defined?(@@project_dir) + + @@initial_reset = false ## # #setup prepares a sandboxed location to install gems. All installs are @@ -119,8 +118,8 @@ class Gem::TestCase < MiniTest::Unit::TestCase @current_dir = Dir.pwd @ui = Gem::MockGemUi.new - tmpdir = nil - Dir.chdir Dir.tmpdir do tmpdir = Dir.pwd end # HACK OSX /private/tmp + # Need to do this in the project because $SAFE fucks up _everything_ + tmpdir = File.expand_path("tmp/test") if ENV['KEEP_FILES'] then @tempdir = File.join(tmpdir, "test_rubygems_#{$$}.#{Time.now.to_i}") @@ -132,14 +131,14 @@ class Gem::TestCase < MiniTest::Unit::TestCase @userhome = File.join @tempdir, 'userhome' @orig_ruby = if ruby = ENV['RUBY'] then - Gem.class_eval { ruby, @ruby = @ruby, ruby } + Gem.class_eval { ruby, @ruby = @ruby, ruby.dup } ruby end Gem.ensure_gem_subdirectories @gemhome @orig_LOAD_PATH = $LOAD_PATH.dup - $LOAD_PATH.map! { |s| File.expand_path s } + $LOAD_PATH.map! { |s| File.expand_path(s).untaint } Dir.chdir @tempdir @@ -150,21 +149,39 @@ class Gem::TestCase < MiniTest::Unit::TestCase FileUtils.mkdir_p @gemhome FileUtils.mkdir_p @userhome + @default_dir = File.join @tempdir, 'default' + @default_spec_dir = File.join @default_dir, "specifications", "default" + Gem.instance_variable_set :@default_dir, @default_dir + FileUtils.mkdir_p @default_spec_dir + + # We use Gem::Specification.reset the first time only so that if there + # are unresolved deps that leak into the whole test suite, they're at least + # reported once. + if @@initial_reset + Gem::Specification.unresolved_deps.clear # done to avoid cross-test warnings + else + @@initial_reset = true + Gem::Specification.reset + end Gem.use_paths(@gemhome) + Gem::Security.reset + Gem.loaded_specs.clear - Gem.unresolved_deps.clear + Gem.clear_default_specs + Gem::Specification.unresolved_deps.clear Gem.configuration.verbose = true Gem.configuration.update_sources = true + Gem::RemoteFetcher.fetcher = Gem::FakeFetcher.new + @gem_repo = "http://gems.example.com/" @uri = URI.parse @gem_repo Gem.sources.replace [@gem_repo] Gem.searcher = nil Gem::SpecFetcher.fetcher = nil - @orig_BASERUBY = Gem::ConfigMap[:BASERUBY] Gem::ConfigMap[:BASERUBY] = Gem::ConfigMap[:ruby_install_name] @@ -181,8 +198,11 @@ class Gem::TestCase < MiniTest::Unit::TestCase # TODO: move to installer test cases Gem.post_build_hooks.clear Gem.post_install_hooks.clear + Gem.done_installing_hooks.clear + Gem.post_reset_hooks.clear Gem.post_uninstall_hooks.clear Gem.pre_install_hooks.clear + Gem.pre_reset_hooks.clear Gem.pre_uninstall_hooks.clear # TODO: move to installer test cases @@ -207,9 +227,6 @@ class Gem::TestCase < MiniTest::Unit::TestCase Gem.pre_uninstall do |uninstaller| @pre_uninstall_hook_arg = uninstaller end - - @orig_yamler = YAML::ENGINE.yamler - YAML::ENGINE.yamler = 'psych' rescue nil end ## @@ -217,7 +234,7 @@ class Gem::TestCase < MiniTest::Unit::TestCase # tempdir unless the +KEEP_FILES+ environment variable was set. def teardown - $LOAD_PATH.replace @orig_LOAD_PATH + $LOAD_PATH.replace @orig_LOAD_PATH if @orig_LOAD_PATH Gem::ConfigMap[:BASERUBY] = @orig_BASERUBY Gem::ConfigMap[:arch] = @orig_arch @@ -242,7 +259,7 @@ class Gem::TestCase < MiniTest::Unit::TestCase ENV.delete 'HOME' end - YAML::ENGINE.yamler = @orig_yamler + Gem.instance_variable_set :@default_dir, nil end ## @@ -251,13 +268,17 @@ class Gem::TestCase < MiniTest::Unit::TestCase def install_gem spec, options = {} require 'rubygems/installer' - use_ui Gem::MockGemUi.new do - Dir.chdir @tempdir do - Gem::Builder.new(spec).build + gem = File.join @tempdir, "gems", "#{spec.full_name}.gem" + + unless File.exists? gem + use_ui Gem::MockGemUi.new do + Dir.chdir @tempdir do + Gem::Package.build spec + end end - end - gem = File.join(@tempdir, File.basename(spec.cache_file)).untaint + gem = File.join(@tempdir, File.basename(spec.cache_file)).untaint + end Gem::Installer.new(gem, options.merge({:wrappers => true})).install end @@ -280,6 +301,7 @@ class Gem::TestCase < MiniTest::Unit::TestCase ## # creates a temporary directory with hax + # TODO: deprecate and remove def create_tmpdir tmpdir = nil @@ -340,8 +362,7 @@ class Gem::TestCase < MiniTest::Unit::TestCase # homepage, summary and description are defaulted. The specification is # yielded for customization. # - # The gem is added to the installed gems in +@gemhome+ and to the current - # source_index. + # The gem is added to the installed gems in +@gemhome+ and the runtime. # # Use this with #write_file to build an installed gem. @@ -414,7 +435,7 @@ class Gem::TestCase < MiniTest::Unit::TestCase end use_ui Gem::MockGemUi.new do - Gem::Builder.new(spec).build + Gem::Package.build spec end cache = spec.cache_file @@ -422,6 +443,11 @@ class Gem::TestCase < MiniTest::Unit::TestCase end end + def util_remove_gem(spec) + FileUtils.rm_rf spec.cache_file + FileUtils.rm_rf spec.spec_file + end + ## # Removes all installed gems from +@gemhome+. @@ -439,6 +465,16 @@ class Gem::TestCase < MiniTest::Unit::TestCase Gem.searcher = nil end + ## + # Install the provided default specs + + def install_default_specs(*specs) + install_specs(*specs) + specs.each do |spec| + Gem.register_default_spec(spec) + end + end + ## # Create a new spec (or gem if passed an array of files) and set it # up properly. Use this instead of util_spec and util_gem. @@ -483,6 +519,24 @@ class Gem::TestCase < MiniTest::Unit::TestCase spec end + def new_default_spec(name, version, deps = nil, *files) + spec = new_spec(name, version, deps) + spec.loaded_from = File.join(@default_spec_dir, spec.spec_name) + spec.files = files + + lib_dir = File.join(@tempdir, "default_gems", "lib") + $LOAD_PATH.unshift(lib_dir) + files.each do |file| + rb_path = File.join(lib_dir, file) + FileUtils.mkdir_p(File.dirname(rb_path)) + File.open(rb_path, "w") do |rb| + rb << "# #{file}" + end + end + + spec + end + ## # Creates a spec with +name+, +version+ and +deps+. @@ -597,6 +651,12 @@ Also, a list: @a_evil9 = quick_gem('a_evil', '9', &init) @b2 = quick_gem('b', '2', &init) @c1_2 = quick_gem('c', '1.2', &init) + @x = quick_gem('x', '1', &init) + @dep_x = quick_gem('dep_x', '1') do |s| + s.files = %w[lib/code.rb] + s.require_paths = %w[lib] + s.add_dependency 'x', '>= 1' + end @pl1 = quick_gem 'pl', '1' do |s| # l for legacy s.files = %w[lib/code.rb] @@ -617,8 +677,10 @@ Also, a list: write_file File.join(*%W[gems #{@b2.original_name} lib code.rb]) write_file File.join(*%W[gems #{@c1_2.original_name} lib code.rb]) write_file File.join(*%W[gems #{@pl1.original_name} lib code.rb]) + write_file File.join(*%W[gems #{@x.original_name} lib code.rb]) + write_file File.join(*%W[gems #{@dep_x.original_name} lib code.rb]) - [@a1, @a2, @a3a, @a_evil9, @b2, @c1_2, @pl1].each do |spec| + [@a1, @a2, @a3a, @a_evil9, @b2, @c1_2, @pl1, @x, @dep_x].each do |spec| util_build_gem spec end @@ -663,6 +725,15 @@ Also, a list: Gem::RemoteFetcher.fetcher = @fetcher end + ## + # Add +spec+ to +@fetcher+ serving the data in the file +path+. + # +repo+ indicates which repo to make +spec+ appear to be in. + + def add_to_fetcher(spec, path=nil, repo=@gem_repo) + path ||= spec.cache_file + @fetcher.data["#{@gem_repo}gems/#{spec.file_name}"] = read_binary(path) + end + ## # Sets up Gem::SpecFetcher to return information from the gems in +specs+. # Best used with +@all_gems+ from #util_setup_fake_fetcher. @@ -673,26 +744,23 @@ Also, a list: spec_fetcher = Gem::SpecFetcher.fetcher - prerelease, _ = Gem::Specification.partition { |spec| + prerelease, all = Gem::Specification.partition { |spec| spec.version.prerelease? } spec_fetcher.specs[@uri] = [] - Gem::Specification.each do |spec| - spec_tuple = [spec.name, spec.version, spec.original_platform] - spec_fetcher.specs[@uri] << spec_tuple + all.each do |spec| + spec_fetcher.specs[@uri] << spec.name_tuple end spec_fetcher.latest_specs[@uri] = [] Gem::Specification.latest_specs.each do |spec| - spec_tuple = [spec.name, spec.version, spec.original_platform] - spec_fetcher.latest_specs[@uri] << spec_tuple + spec_fetcher.latest_specs[@uri] << spec.name_tuple end spec_fetcher.prerelease_specs[@uri] = [] prerelease.each do |spec| - spec_tuple = [spec.name, spec.version, spec.original_platform] - spec_fetcher.prerelease_specs[@uri] << spec_tuple + spec_fetcher.prerelease_specs[@uri] << spec.name_tuple end v = Gem.marshal_version @@ -769,6 +837,14 @@ Also, a list: system('nmake /? 1>NUL 2>&1') end + # In case we're building docs in a background process, this method waits for + # that process to exit (or if it's already been reaped, or never happened, + # swallows the Errno::ECHILD error). + def wait_for_child_process_to_exit + Process.wait if Process.respond_to?(:fork) + rescue Errno::ECHILD + end + ## # Allows tests to use a random (but controlled) port number instead of # a hardcoded one. This helps CI tools when running parallels builds on @@ -788,12 +864,13 @@ Also, a list: ## # Allows the proper version of +rake+ to be used for the test. - def build_rake_in + def build_rake_in(good=true) gem_ruby = Gem.ruby Gem.ruby = @@ruby env_rake = ENV["rake"] - ENV["rake"] = @@rake - yield @@rake + rake = (good ? @@good_rake : @@bad_rake) + ENV["rake"] = rake + yield rake ensure Gem.ruby = gem_ruby if env_rake @@ -833,15 +910,8 @@ Also, a list: end @@ruby = rubybin - env_rake = ENV['rake'] - ruby19_rake = File.expand_path("bin/rake", @@project_dir) - @@rake = if env_rake then - ENV["rake"] - elsif File.exist? ruby19_rake then - @@ruby + " " + ruby19_rake - else - 'rake' - end + @@good_rake = "#{rubybin} #{File.expand_path('../../../test/rubygems/good_rake.rb', __FILE__)}" + @@bad_rake = "#{rubybin} #{File.expand_path('../../../test/rubygems/bad_rake.rb', __FILE__)}" ## # Construct a new Gem::Dependency. @@ -872,4 +942,73 @@ Also, a list: Gem::Version.create string end + class StaticSet + def initialize(specs) + @specs = specs.sort_by { |s| s.full_name } + end + + def find_spec(dep) + @specs.reverse_each do |s| + return s if dep.matches_spec? s + end + end + + def find_all(dep) + @specs.find_all { |s| dep.matches_spec? s } + end + + def prefetch(reqs) + end + end + + ## + # Loads certificate named +cert_name+ from test/rubygems/. + + def self.load_cert cert_name + cert_file = cert_path cert_name + + cert = File.read cert_file + + OpenSSL::X509::Certificate.new cert + end + + ## + # Returns the path to the certificate named +cert_name+ from + # test/rubygems/. + + def self.cert_path cert_name + if 32 == (Time.at(2**32) rescue 32) then + cert_file = + File.expand_path "../../../test/rubygems/#{cert_name}_cert_32.pem", + __FILE__ + + return cert_file if File.exist? cert_file + end + + File.expand_path "../../../test/rubygems/#{cert_name}_cert.pem", __FILE__ + end + + ## + # Loads an RSA private key named +key_name+ in test/rubygems/ + + def self.load_key key_name + key_file = key_path key_name + + key = File.read key_file + + OpenSSL::PKey::RSA.new key + end + + ## + # Returns the path tot he key named +key_name+ from test/rubygems + + def self.key_path key_name + File.expand_path "../../../test/rubygems/#{key_name}_key.pem", __FILE__ + end + + PRIVATE_KEY = load_key 'private' + PUBLIC_KEY = PRIVATE_KEY.public_key + + PUBLIC_CERT = load_cert 'public' + end diff --git a/lib/rubygems/test_utilities.rb b/lib/rubygems/test_utilities.rb index 1a8fb5a0ad..bd5f2134be 100644 --- a/lib/rubygems/test_utilities.rb +++ b/lib/rubygems/test_utilities.rb @@ -24,11 +24,17 @@ class Gem::FakeFetcher attr_reader :data attr_reader :last_request + attr_reader :api_endpoints attr_accessor :paths def initialize @data = {} @paths = [] + @api_endpoints = {} + end + + def api_endpoint(uri) + @api_endpoints[uri] || uri end def find_data(path) @@ -57,6 +63,15 @@ class Gem::FakeFetcher end end + def cache_update_path uri, path = nil + if data = fetch_path(uri) + open(path, 'wb') { |io| io.write data } if path + data + else + Gem.read_binary(path) if path + end + end + # Thanks, FakeWeb! def open_uri_or_path(path) data = find_data(path) @@ -98,7 +113,13 @@ class Gem::FakeFetcher def download spec, source_uri, install_dir = Gem.dir name = File.basename spec.cache_file - path = File.join install_dir, "cache", name + path = if Dir.pwd == install_dir then # see fetch_command + install_dir + else + File.join install_dir, "cache" + end + + path = File.join path, name Gem.ensure_gem_subdirectories install_dir @@ -114,14 +135,13 @@ class Gem::FakeFetcher end def download_to_cache dependency - found = Gem::SpecFetcher.fetcher.fetch dependency, true, true, - dependency.prerelease? + found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dependency return if found.empty? - spec, source_uri = found.first + spec, source = found.first - download spec, source_uri + download spec, source.uri.to_s end end diff --git a/lib/rubygems/uninstaller.rb b/lib/rubygems/uninstaller.rb index cc32ea48c4..b1500b0748 100644 --- a/lib/rubygems/uninstaller.rb +++ b/lib/rubygems/uninstaller.rb @@ -7,7 +7,7 @@ require 'fileutils' require 'rubygems' require 'rubygems/dependency_list' -require 'rubygems/doc_manager' +require 'rubygems/rdoc' require 'rubygems/user_interaction' ## @@ -42,6 +42,7 @@ class Gem::Uninstaller # Constructs an uninstaller that will uninstall +gem+ def initialize(gem, options = {}) + # TODO document the valid options @gem = gem @version = options[:version] || Gem::Requirement.default @gem_home = File.expand_path(options[:install_dir] || Gem.dir) @@ -51,15 +52,19 @@ class Gem::Uninstaller @bin_dir = options[:bin_dir] @format_executable = options[:format_executable] + # Indicate if development dependencies should be checked when + # uninstalling. (default: false) + # + @check_dev = options[:check_dev] + + if options[:force] + @force_all = true + @force_ignore = true + end + # only add user directory if install_dir is not set @user_install = false @user_install = options[:user_install] unless options[:install_dir] - - if @user_install then - Gem.use_paths Gem.user_dir, @gem_home - else - Gem.use_paths @gem_home - end end ## @@ -69,10 +74,36 @@ class Gem::Uninstaller def uninstall list = Gem::Specification.find_all_by_name(@gem, @version) + default_specs, list = list.partition do |spec| + spec.default_gem? + end + + list, other_repo_specs = list.partition do |spec| + @gem_home == spec.base_dir or + (@user_install and spec.base_dir == Gem.user_dir) + end + if list.empty? then - raise Gem::InstallError, "gem #{@gem.inspect} is not installed" + if other_repo_specs.empty? + if default_specs.empty? + raise Gem::InstallError, "gem #{@gem.inspect} is not installed" + else + message = + "gem #{@gem.inspect} cannot be uninstalled " + + "because it is a default gem" + raise Gem::InstallError, message + end + end + + other_repos = other_repo_specs.map { |spec| spec.base_dir }.uniq - elsif list.size > 1 and @force_all then + message = ["#{@gem} is not installed in GEM_HOME, try:"] + message.concat other_repos.map { |repo| + "\tgem uninstall -i #{repo} #{@gem}" + } + + raise Gem::InstallError, message.join("\n") + elsif @force_all then remove_all list elsif list.size > 1 then @@ -127,12 +158,15 @@ class Gem::Uninstaller def remove_executables(spec) return if spec.nil? or spec.executables.empty? + executables = spec.executables.clone + + # Leave any executables created by other installed versions + # of this gem installed. + list = Gem::Specification.find_all { |s| s.name == spec.name && s.version != spec.version } - executables = spec.executables.clone - list.each do |s| s.executables.each do |exe_name| executables.delete exe_name @@ -152,9 +186,7 @@ class Gem::Uninstaller @force_executables end - unless remove then - say "Executables and scripts will remain installed." - else + if remove then bin_dir = @bin_dir || Gem.bindir(spec.base_dir) raise Gem::FilePermissionError, bin_dir unless File.writable? bin_dir @@ -167,6 +199,8 @@ class Gem::Uninstaller FileUtils.rm_f exe_file FileUtils.rm_f "#{exe_file}.bat" end + else + say "Executables and scripts will remain installed." end end @@ -220,7 +254,7 @@ class Gem::Uninstaller FileUtils.rm_rf gem - Gem::DocManager.new(spec).uninstall_doc + Gem::RDoc.new(spec).remove say "Successfully uninstalled #{spec.full_name}" @@ -241,26 +275,34 @@ class Gem::Uninstaller return true if @force_ignore deplist = Gem::DependencyList.from_specs - deplist.ok_to_remove?(spec.full_name) + deplist.ok_to_remove?(spec.full_name, @check_dev) end def ask_if_ok(spec) msg = [''] msg << 'You have requested to uninstall the gem:' msg << "\t#{spec.full_name}" + msg << '' + + siblings = Gem::Specification.select do |s| + s.name == spec.name && s.full_name != spec.full_name + end spec.dependent_gems.each do |dep_spec, dep, satlist| - msg << - ("#{dep_spec.name}-#{dep_spec.version} depends on " + - "[#{dep.name} (#{dep.requirement})]") + unless siblings.any? { |s| s.satisfies_requirement? dep } + msg << "#{dep_spec.name}-#{dep_spec.version} depends on #{dep}" + end end - msg << 'If you remove this gems, one or more dependencies will not be met.' + msg << 'If you remove this gem, these dependencies will not be met.' msg << 'Continue with Uninstall?' return ask_yes_no(msg.join("\n"), true) end def formatted_program_filename(filename) + # TODO perhaps the installer should leave a small manifest + # of what it did for us to find rather than trying to recreate + # it again. if @format_executable then require 'rubygems/installer' Gem::Installer.exec_format % File.basename(filename) diff --git a/lib/rubygems/user_interaction.rb b/lib/rubygems/user_interaction.rb index 8024d37287..0974476507 100644 --- a/lib/rubygems/user_interaction.rb +++ b/lib/rubygems/user_interaction.rb @@ -143,6 +143,16 @@ class Gem::StreamUI end end + ## + # Prints a formatted backtrace to the errors stream if backtraces are + # enabled. + + def backtrace exception + return unless Gem.configuration.backtrace + + @errs.puts "\t#{exception.backtrace.join "\n\t"}" + end + ## # Choose from a list of options. +question+ is a prompt displayed above # the list. +list+ is a list of option strings. Returns the pair diff --git a/lib/rubygems/validator.rb b/lib/rubygems/validator.rb index ffeed88660..f66b2c1f43 100644 --- a/lib/rubygems/validator.rb +++ b/lib/rubygems/validator.rb @@ -4,7 +4,7 @@ # See LICENSE.txt for permissions. #++ -require 'rubygems/format' +require 'rubygems/package' require 'rubygems/installer' ## @@ -16,7 +16,6 @@ class Gem::Validator def initialize require 'find' - require 'digest' end ## @@ -24,20 +23,8 @@ class Gem::Validator # gem_data:: [String] Contents of the gem file def verify_gem(gem_data) - raise Gem::VerificationError, 'empty gem file' if gem_data.size == 0 - - unless gem_data =~ /MD5SUM/ then - return # Don't worry about it...this sucks. Need to fix MD5 stuff for - # new format - # FIXME - end - - sum_data = gem_data.gsub(/MD5SUM = "([a-z0-9]+)"/, - "MD5SUM = \"#{"F" * 32}\"") - - unless Digest::MD5.hexdigest(sum_data) == $1.to_s then - raise Gem::VerificationError, 'invalid checksum for gem file' - end + # TODO remove me? The code here only validate an MD5SUM that was + # in some old formatted gems, but hasn't been for a long time. end ## @@ -58,17 +45,27 @@ class Gem::Validator def find_files_for_gem(gem_directory) installed_files = [] + Find.find gem_directory do |file_name| fn = file_name[gem_directory.size..file_name.size-1].sub(/^\//, "") installed_files << fn unless fn =~ /CVS/ || fn.empty? || File.directory?(file_name) end + installed_files end public - ErrorData = Struct.new :path, :problem + ErrorData = Struct.new :path, :problem do + + def <=> other + return nil unless self.class === other + + [path, problem] <=> [other.path, other.problem] + end + + end ## # Checks the gem directory for the following potential @@ -80,20 +77,22 @@ class Gem::Validator # * 1 cache - 1 spec - 1 directory. # # returns a hash of ErrorData objects, keyed on the problem gem's name. + #-- + # TODO needs further cleanup def alien(gems=[]) errors = Hash.new { |h,k| h[k] = {} } - Gem::SourceIndex.from_installed_gems.each do |gem_name, gem_spec| - next unless gems.include? gem_spec.name unless gems.empty? + Gem::Specification.each do |spec| + next unless gems.include? spec.name unless gems.empty? - install_dir = gem_spec.installation_path - gem_path = Gem.cache_gem(gem_spec.file_name, install_dir) - spec_path = File.join install_dir, "specifications", gem_spec.spec_name - gem_directory = gem_spec.full_gem_path + gem_name = spec.file_name + gem_path = spec.cache_file + spec_path = spec.spec_file + gem_directory = spec.full_gem_path unless File.directory? gem_directory then - errors[gem_name][gem_spec.full_name] = + errors[gem_name][spec.full_name] = "Gem registered but doesn't exist at #{gem_directory}" next end @@ -108,19 +107,18 @@ class Gem::Validator good, gone, unreadable = nil, nil, nil, nil open gem_path, Gem.binary_mode do |file| - format = Gem::Format.from_file_by_path(gem_path) + package = Gem::Package.new gem_path - good, gone = format.file_entries.partition { |entry, _| - File.exist? File.join(gem_directory, entry['path']) + good, gone = package.contents.partition { |file_name| + File.exist? File.join(gem_directory, file_name) } - gone.map! { |entry, _| entry['path'] } gone.sort.each do |path| errors[gem_name][path] = "Missing file" end - good, unreadable = good.partition { |entry, _| - File.readable? File.join(gem_directory, entry['path']) + good, unreadable = good.partition { |file_name| + File.readable? File.join(gem_directory, file_name) } unreadable.map! { |entry, _| entry['path'] } @@ -132,9 +130,10 @@ class Gem::Validator begin next unless data # HACK `gem check -a mkrf` - open File.join(gem_directory, entry['path']), Gem.binary_mode do |f| - unless Digest::MD5.hexdigest(f.read).to_s == - Digest::MD5.hexdigest(data).to_s then + source = File.join gem_directory, entry['path'] + + open source, Gem.binary_mode do |f| + unless f.read == data then errors[gem_name][entry['path']] = "Modified from original" end end @@ -143,7 +142,6 @@ class Gem::Validator end installed_files = find_files_for_gem(gem_directory) - good.map! { |entry, _| entry['path'] } extras = installed_files - good - unreadable extras.each do |extra| @@ -155,15 +153,10 @@ class Gem::Validator end errors.each do |name, subhash| - errors[name] = subhash.map { |path, msg| ErrorData.new(path, msg) } + errors[name] = subhash.map { |path, msg| ErrorData.new(path, msg) }.sort end errors end - - def remove_leading_dot_dir(path) - path.sub(/^\.\//, "") - end - end diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb index 2ced9ccdfb..e983751c17 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -129,8 +129,8 @@ # specify your dependency as ">= 2.0.0" then, you're good, right? What # happens if fnord 3.0 comes out and it isn't backwards compatible # with 2.y.z? Your stuff will break as a result of using ">=". The -# better route is to specify your dependency with a "spermy" version -# specifier. They're a tad confusing, so here is how the dependency +# better route is to specify your dependency with an "approximate" version +# specifier ("~>"). They're a tad confusing, so here is how the dependency # specifiers work: # # Specification From ... To (exclusive) @@ -145,6 +145,8 @@ class Gem::Version include Comparable + # FIX: These are only used once, in .correct?. Do they deserve to be + # constants? VERSION_PATTERN = '[0-9]+(\.[0-9a-zA-Z]+)*' # :nodoc: ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})*\s*\z/ # :nodoc: @@ -169,6 +171,8 @@ class Gem::Version # ver2 = Version.create(ver1) # -> (ver1) # ver3 = Version.create(nil) # -> nil + # REFACTOR: There's no real reason this should be separate from #initialize. + def self.create input if input.respond_to? :version then input @@ -187,8 +191,7 @@ class Gem::Version raise ArgumentError, "Malformed version number string #{version}" unless self.class.correct?(version) - @version = version.to_s - @version.strip! + @version = version.to_s.dup.strip end ## @@ -248,11 +251,19 @@ class Gem::Version @hash = nil end + def to_yaml_properties + ["@version"] + end + + def encode_with coder + coder.add 'version', @version + end + ## # A version is considered a prerelease if it contains a letter. def prerelease? - @prerelease ||= @version =~ /[a-zA-Z]/ + @prerelease ||= !!(@version =~ /[a-zA-Z]/) end def pretty_print q # :nodoc: @@ -284,7 +295,7 @@ class Gem::Version ## # A recommended version for use with a ~> Requirement. - def spermy_recommendation + def approximate_recommendation segments = self.segments.dup segments.pop while segments.any? { |s| String === s } -- cgit v1.2.3