From 269503b544247b5b3e30dbe60a0bab4f2ca00e4e Mon Sep 17 00:00:00 2001 From: naruse Date: Sat, 14 Sep 2013 08:59:02 +0000 Subject: Revert r42938 "* lib/rubygems: Update to RubyGems 2.1.3" It breaks build. http://u64.rubyci.org/~chkbuild/ruby-trunk/log/20130913T200302Z.diff.html.gz git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@42941 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- lib/rubygems/available_set.rb | 68 +++ lib/rubygems/command_manager.rb | 77 ++- lib/rubygems/commands/build_command.rb | 19 + lib/rubygems/commands/cert_command.rb | 107 +++- lib/rubygems/commands/check_command.rb | 7 + lib/rubygems/commands/cleanup_command.rb | 14 +- lib/rubygems/commands/contents_command.rb | 167 ++++-- lib/rubygems/commands/dependency_command.rb | 158 ++++-- lib/rubygems/commands/environment_command.rb | 126 +++-- lib/rubygems/commands/fetch_command.rb | 16 +- lib/rubygems/commands/help_command.rb | 140 +++-- lib/rubygems/commands/install_command.rb | 126 +++-- lib/rubygems/commands/list_command.rb | 13 +- lib/rubygems/commands/mirror_command.rb | 6 + lib/rubygems/commands/outdated_command.rb | 21 +- lib/rubygems/commands/owner_command.rb | 17 +- lib/rubygems/commands/pristine_command.rb | 19 +- lib/rubygems/commands/push_command.rb | 10 +- lib/rubygems/commands/query_command.rb | 9 + lib/rubygems/commands/rdoc_command.rb | 8 +- lib/rubygems/commands/search_command.rb | 15 +- lib/rubygems/commands/sources_command.rb | 202 ++++--- lib/rubygems/commands/specification_command.rb | 16 + lib/rubygems/commands/stale_command.rb | 10 + lib/rubygems/commands/uninstall_command.rb | 47 +- lib/rubygems/commands/unpack_command.rb | 18 + lib/rubygems/commands/update_command.rb | 195 ++++--- lib/rubygems/commands/which_command.rb | 11 + lib/rubygems/commands/yank_command.rb | 18 +- lib/rubygems/config_file.rb | 17 +- lib/rubygems/core_ext/kernel_require.rb | 30 +- lib/rubygems/defaults.rb | 15 + lib/rubygems/dependency.rb | 6 +- lib/rubygems/dependency_installer.rb | 339 ++++++------ lib/rubygems/dependency_resolver.rb | 721 +++++++------------------ lib/rubygems/exceptions.rb | 89 ++- lib/rubygems/ext/builder.rb | 120 +++- lib/rubygems/gem_runner.rb | 26 +- lib/rubygems/gemcutter_utilities.rb | 114 ++-- lib/rubygems/install_update_options.rb | 3 + lib/rubygems/installer.rb | 148 ++--- lib/rubygems/name_tuple.rb | 25 +- lib/rubygems/package.rb | 81 ++- lib/rubygems/package/tar_test_case.rb | 18 +- lib/rubygems/package/tar_writer.rb | 47 +- lib/rubygems/path_support.rb | 10 + lib/rubygems/platform.rb | 12 +- lib/rubygems/psych_additions.rb | 2 +- lib/rubygems/remote_fetcher.rb | 266 +-------- lib/rubygems/request_set.rb | 251 ++++----- lib/rubygems/security.rb | 55 +- lib/rubygems/security/policy.rb | 49 +- lib/rubygems/security/signer.rb | 24 +- lib/rubygems/server.rb | 6 +- lib/rubygems/source.rb | 36 +- lib/rubygems/source_local.rb | 91 +--- lib/rubygems/source_specific_file.rb | 28 +- lib/rubygems/spec_fetcher.rb | 20 +- lib/rubygems/specification.rb | 291 +++++----- lib/rubygems/test_case.rb | 200 +++++-- lib/rubygems/uninstaller.rb | 23 +- lib/rubygems/version.rb | 16 +- lib/rubygems/version_option.rb | 10 +- 63 files changed, 2676 insertions(+), 2173 deletions(-) (limited to 'lib/rubygems') diff --git a/lib/rubygems/available_set.rb b/lib/rubygems/available_set.rb index 80539feee9..bb0b3a3abe 100644 --- a/lib/rubygems/available_set.rb +++ b/lib/rubygems/available_set.rb @@ -1,4 +1,7 @@ class Gem::AvailableSet + + include Enumerable + Tuple = Struct.new(:spec, :source) def initialize @@ -36,6 +39,28 @@ class Gem::AvailableSet self end + ## + # Yields each Tuple in this AvailableSet + + def each + return enum_for __method__ unless block_given? + + @set.each do |tuple| + yield tuple + end + end + + ## + # Yields the Gem::Specification for each Tuple in this AvailableSet + + def each_spec + return enum_for __method__ unless block_given? + + each do |tuple| + yield tuple.spec + end + end + def empty? @set.empty? end @@ -66,6 +91,49 @@ class Gem::AvailableSet f.source end + ## + # Converts this AvailableSet into a RequestSet that can be used to install + # gems. + # + # If +development+ is :none then no development dependencies are installed. + # Other options are :shallow for only direct development dependencies of the + # gems in this set or :all for all development dependencies. + + def to_request_set development = :none + request_set = Gem::RequestSet.new + request_set.development = :all == development + + each_spec do |spec| + request_set.always_install << spec + + request_set.gem spec.name, spec.version + request_set.import spec.development_dependencies if + :shallow == development + end + + request_set + end + + ## + # + # Used by the DependencyResolver, the protocol to use a AvailableSet as a + # search Set. + + def find_all(req) + dep = req.dependency + + match = @set.find_all do |t| + dep.matches_spec? t.spec + end + + match.map do |t| + Gem::DependencyResolver::InstalledSpecification.new(self, t.spec, t.source) + end + end + + def prefetch(reqs) + end + def pick_best! return self if empty? diff --git a/lib/rubygems/command_manager.rb b/lib/rubygems/command_manager.rb index 2af582177d..fdee064fed 100644 --- a/lib/rubygems/command_manager.rb +++ b/lib/rubygems/command_manager.rb @@ -33,6 +33,39 @@ class Gem::CommandManager include Gem::UserInteraction + BUILTIN_COMMANDS = [ # :nodoc: + :build, + :cert, + :check, + :cleanup, + :contents, + :dependency, + :environment, + :fetch, + :generate_index, + :help, + :install, + :list, + :lock, + :mirror, + :outdated, + :owner, + :pristine, + :push, + :query, + :rdoc, + :search, + :server, + :sources, + :specification, + :stale, + :uninstall, + :unpack, + :update, + :which, + :yank, + ] + ## # Return the authoritative instance of the command manager. @@ -61,36 +94,10 @@ class Gem::CommandManager def initialize require 'timeout' @commands = {} - register_command :build - register_command :cert - register_command :check - register_command :cleanup - register_command :contents - register_command :dependency - register_command :environment - register_command :fetch - register_command :generate_index - register_command :help - register_command :install - register_command :list - register_command :lock - register_command :mirror - register_command :outdated - register_command :owner - register_command :pristine - register_command :push - register_command :query - register_command :rdoc - register_command :search - register_command :server - register_command :sources - register_command :specification - register_command :stale - register_command :uninstall - register_command :unpack - register_command :update - register_command :which - register_command :yank + + BUILTIN_COMMANDS.each do |name| + register_command name + end end ## @@ -132,14 +139,6 @@ class Gem::CommandManager alert_error "While executing gem ... (#{ex.class})\n #{ex.to_s}" 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" @@ -147,8 +146,6 @@ class Gem::CommandManager end def process_args(args, build_args=nil) - args = args.to_str.split(/\s+/) if args.respond_to?(:to_str) - if args.empty? then say Gem::Command::HELP terminate_interaction 1 diff --git a/lib/rubygems/commands/build_command.rb b/lib/rubygems/commands/build_command.rb index 64563ed3db..d975429fe8 100644 --- a/lib/rubygems/commands/build_command.rb +++ b/lib/rubygems/commands/build_command.rb @@ -15,6 +15,25 @@ class Gem::Commands::BuildCommand < Gem::Command "GEMSPEC_FILE gemspec file name to build a gem for" end + def description # :nodoc: + <<-EOF +The build command allows you to create a gem from a ruby gemspec. + +The best way to build a gem is to use a Rakefile and the Gem::PackageTask +which ships with RubyGems. + +The gemspec can either be created by hand or extracted from an existing gem +with gem spec: + + $ gem unpack my_gem-1.0.gem + Unpacked gem: '.../my_gem-1.0' + $ gem spec my_gem-1.0.gem --ruby > my_gem-1.0/my_gem-1.0.gemspec + $ cd my_gem-1.0 + [edit gem contents] + $ gem build my_gem-1.0.gemspec + EOF + end + def usage # :nodoc: "#{program_name} GEMSPEC_FILE" end diff --git a/lib/rubygems/commands/cert_command.rb b/lib/rubygems/commands/cert_command.rb index 5a9320f9c4..e417193bca 100644 --- a/lib/rubygems/commands/cert_command.rb +++ b/lib/rubygems/commands/cert_command.rb @@ -1,6 +1,11 @@ require 'rubygems/command' require 'rubygems/security' -require 'openssl' +begin + require 'openssl' +rescue LoadError => e + raise unless (e.respond_to?(:path) && e.path == 'openssl') || + e.message =~ / -- openssl$/ +end class Gem::Commands::CertCommand < Gem::Command @@ -21,7 +26,8 @@ class Gem::Commands::CertCommand < Gem::Command OptionParser.accept OpenSSL::PKey::RSA do |key_file| begin - key = OpenSSL::PKey::RSA.new File.read key_file + passphrase = ENV['GEM_PRIVATE_KEY_PASSPHRASE'] + key = OpenSSL::PKey::RSA.new File.read(key_file), passphrase rescue Errno::ENOENT raise OptionParser::InvalidArgument, "#{key_file}: does not exist" rescue OpenSSL::PKey::RSAError @@ -79,52 +85,67 @@ class Gem::Commands::CertCommand < Gem::Command end end + def add_certificate certificate # :nodoc: + Gem::Security.trust_dir.trust_cert certificate + + say "Added '#{certificate.subject}'" + end + def execute options[:add].each do |certificate| - Gem::Security.trust_dir.trust_cert certificate - - say "Added '#{certificate.subject}'" + add_certificate certificate end options[:remove].each do |filter| - certificates_matching filter do |certificate, path| - FileUtils.rm path - say "Removed '#{certificate.subject}'" - end + remove_certificates_matching filter end options[:list].each do |filter| - certificates_matching filter do |certificate, _| - # this could probably be formatted more gracefully - say certificate.subject.to_s - end + list_certificates_matching filter 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 + sign_certificates unless options[:sign].empty? end def build name - key = options[:key] || Gem::Security.create_key + key, key_path = build_key + cert_path = build_cert name, key + say "Certificate: #{cert_path}" + + if key_path + say "Private Key: #{key_path}" + say "Don't forget to move the key file to somewhere private!" + end + end + + def build_cert name, key # :nodoc: cert = Gem::Security.create_cert_email name, key + Gem::Security.write cert, "gem-public_cert.pem" + end - key_path = Gem::Security.write key, "gem-private_key.pem" - cert_path = Gem::Security.write cert, "gem-public_cert.pem" + def build_key # :nodoc: + if options[:key] then + options[:key] + else + passphrase = ask_for_password 'Passphrase for your Private Key:' + say "\n" - say "Certificate: #{cert_path}" - say "Private Key: #{key_path}" - say "Don't forget to move the key file to somewhere private!" + passphrase_confirmation = ask_for_password 'Please repeat the passphrase for your Private Key:' + say "\n" + + raise Gem::CommandLineError, + "Passphrase and passphrase confirmation don't match" unless passphrase == passphrase_confirmation + + key = Gem::Security.create_key + key_path = Gem::Security.write key, "gem-private_key.pem", 0600, passphrase + + return key, key_path + end end def certificates_matching filter @@ -179,6 +200,13 @@ For further reading on signing gems see `ri Gem::Security`. EOF end + def list_certificates_matching filter # :nodoc: + certificates_matching filter do |certificate, _| + # this could probably be formatted more gracefully + say certificate.subject.to_s + end + end + def load_default_cert cert_file = File.join Gem.default_cert_path cert = File.read cert_file @@ -198,7 +226,8 @@ For further reading on signing gems see `ri Gem::Security`. def load_default_key key_file = File.join Gem.default_key_path key = File.read key_file - options[:key] = OpenSSL::PKey::RSA.new key + passphrase = ENV['GEM_PRIVATE_KEY_PASSPHRASE'] + options[:key] = OpenSSL::PKey::RSA.new key, passphrase rescue Errno::ENOENT alert_error \ "--private-key not specified and ~/.gem/gem-private_key.pem does not exist" @@ -211,6 +240,18 @@ For further reading on signing gems see `ri Gem::Security`. terminate_interaction 1 end + def load_defaults # :nodoc: + load_default_cert unless options[:issuer_cert] + load_default_key unless options[:key] + end + + def remove_certificates_matching filter # :nodoc: + certificates_matching filter do |certificate, path| + FileUtils.rm path + say "Removed '#{certificate.subject}'" + end + end + def sign cert_file cert = File.read cert_file cert = OpenSSL::X509::Certificate.new cert @@ -225,5 +266,13 @@ For further reading on signing gems see `ri Gem::Security`. Gem::Security.write cert, cert_file, permissions end -end + def sign_certificates # :nodoc: + load_defaults unless options[:sign].empty? + + options[:sign].each do |cert_file| + sign cert_file + end + end + +end if defined?(OpenSSL::SSL) diff --git a/lib/rubygems/commands/check_command.rb b/lib/rubygems/commands/check_command.rb index d7677d47a1..8893b9c3b2 100644 --- a/lib/rubygems/commands/check_command.rb +++ b/lib/rubygems/commands/check_command.rb @@ -79,6 +79,13 @@ class Gem::Commands::CheckCommand < Gem::Command '--gems --alien' end + def description # :nodoc: + <<-EOF +The check command can list and repair problems with installed gems and +specifications and will clean up gems that have been partially uninstalled. + EOF + end + def usage # :nodoc: "#{program_name} [OPTIONS] [GEMNAME ...]" end diff --git a/lib/rubygems/commands/cleanup_command.rb b/lib/rubygems/commands/cleanup_command.rb index 61f189e449..c8f0082bfb 100644 --- a/lib/rubygems/commands/cleanup_command.rb +++ b/lib/rubygems/commands/cleanup_command.rb @@ -6,10 +6,11 @@ class Gem::Commands::CleanupCommand < Gem::Command def initialize super 'cleanup', - 'Clean up old versions of installed gems in the local repository', + 'Clean up old versions of installed gems', :force => false, :install_dir => Gem.dir - add_option('-d', '--dryrun', "") do |value, options| + add_option('-n', '-d', '--dryrun', + 'Do not uninstall gems') do |value, options| options[:dryrun] = true end @@ -32,11 +33,11 @@ class Gem::Commands::CleanupCommand < Gem::Command def description # :nodoc: <<-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. +The cleanup command removes old versions of gems from GEM_HOME that are not +required to meet a dependency. If a gem is installed elsewhere in GEM_PATH +the cleanup command won't delete it. -Older gems that are required to satisify the dependencies of gems -are not removed. +If no gems are named all gems in GEM_HOME are cleaned. EOF end @@ -162,4 +163,3 @@ are not removed. end end - diff --git a/lib/rubygems/commands/contents_command.rb b/lib/rubygems/commands/contents_command.rb index 42c7fabd86..97218848ed 100644 --- a/lib/rubygems/commands/contents_command.rb +++ b/lib/rubygems/commands/contents_command.rb @@ -31,6 +31,10 @@ class Gem::Commands::ContentsCommand < Gem::Command "Don't include installed path prefix") do |prefix, options| options[:prefix] = prefix end + + @path_kind = nil + @spec_dirs = nil + @version = nil end def arguments # :nodoc: @@ -41,79 +45,126 @@ class Gem::Commands::ContentsCommand < Gem::Command "--no-lib-only --prefix" end + def description # :nodoc: + <<-EOF +The contents command lists the files in an installed gem. The listing can +be given as full file names, file names without the installed directory +prefix or only the files that are requireable. + EOF + end + def usage # :nodoc: "#{program_name} GEMNAME [GEMNAME ...]" end def execute - version = options[:version] || Gem::Requirement.default + @version = options[:version] || Gem::Requirement.default + @spec_dirs = specification_directories + @path_kind = path_description @spec_dirs - spec_dirs = options[:specdirs].map do |i| - [i, File.join(i, "specifications")] - end.flatten + names = gem_names - path_kind = if spec_dirs.empty? then - spec_dirs = Gem::Specification.dirs - "default gem paths" - else - "specified path" - end - - gem_names = if options[:all] then - Gem::Specification.map(&:name) - else - get_all_gem_names - end - - gem_names.each do |name| - # HACK: find_by_name fails for some reason... ARGH - # How many places must we embed our resolve logic? - spec = Gem::Specification.find_all_by_name(name, version).last - - unless spec then - say "Unable to find gem '#{name}' in #{path_kind}" - - if Gem.configuration.verbose then - say "\nDirectories searched:" - spec_dirs.sort.each { |dir| say dir } - end - - terminate_interaction 1 if gem_names.length == 1 - end + names.each do |name| + found = gem_contents name - if spec.default_gem? - files = spec.files.sort.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 + terminate_interaction 1 unless found or names.length > 1 + end + end + + def files_in spec + if spec.default_gem? then + files_in_default_gem spec + else + files_in_gem spec + end + end + + def files_in_gem spec + gem_path = spec.full_gem_path + extra = "/{#{spec.require_paths.join ','}}" if options[:lib_only] + glob = "#{gem_path}#{extra}/**/*" + prefix_re = /#{Regexp.escape(gem_path)}\// + + Dir[glob].map do |file| + [gem_path, file.sub(prefix_re, "")] + end + end + + def files_in_default_gem spec + spec.files.sort.map do |file| + case file + when /\A#{spec.bindir}\// + [Gem::ConfigMap[:bindir], $POSTMATCH] + when /\.so\z/ + [Gem::ConfigMap[:archdir], file] 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 + [Gem::ConfigMap[:rubylibdir], file] end + end + end + + def gem_contents name + spec = spec_for name + + return false unless spec + + files = files_in spec - files.sort.each do |prefix, basename| - absolute_path = File.join(prefix, basename) - next if File.directory? absolute_path + show_files files - if options[:prefix] - say absolute_path - else - say basename - end + true + end + + def gem_names # :nodoc: + if options[:all] then + Gem::Specification.map(&:name) + else + get_all_gem_names + end + end + + def path_description spec_dirs # :nodoc: + if spec_dirs.empty? then + spec_dirs = Gem::Specification.dirs + "default gem paths" + else + "specified path" + end + end + + def show_files files + files.sort.each do |prefix, basename| + absolute_path = File.join(prefix, basename) + next if File.directory? absolute_path + + if options[:prefix] then + say absolute_path + else + say basename end end end + def spec_for name + spec = Gem::Specification.find_all_by_name(name, @version).last + + return spec if spec + + say "Unable to find gem '#{name}' in #{@path_kind}" + + if Gem.configuration.verbose then + say "\nDirectories searched:" + @spec_dirs.sort.each { |dir| say dir } + end + + return nil + end + + def specification_directories # :nodoc: + options[:specdirs].map do |i| + [i, File.join(i, "specifications")] + end.flatten + end + end diff --git a/lib/rubygems/commands/dependency_command.rb b/lib/rubygems/commands/dependency_command.rb index 4690b13a94..c5d6dd7d70 100644 --- a/lib/rubygems/commands/dependency_command.rb +++ b/lib/rubygems/commands/dependency_command.rb @@ -38,89 +38,121 @@ class Gem::Commands::DependencyCommand < Gem::Command "--local --version '#{Gem::Requirement.default}' --no-reverse-dependencies" end + def description # :nodoc: + <<-EOF +The dependency commands lists which other gems a given gem depends on. For +local gems only the reverse dependencies can be shown (which gems depend on +the named gem). + +The dependency list can be displayed in a format suitable for piping for +use with other commands. + EOF + end + def usage # :nodoc: "#{program_name} GEMNAME" end - def execute - if options[:reverse_dependencies] and remote? and not local? then - alert_error 'Only reverse dependencies for local gems are supported.' - terminate_interaction 1 - end + def fetch_remote_specs dependency # :nodoc: + fetcher = Gem::SpecFetcher.fetcher + + ss, = fetcher.spec_for_dependency dependency + + ss.map { |spec, _| spec } + end + + def fetch_specs dependency # :nodoc: + specs = [] + + specs.concat dependency.matching_specs if local? + specs.concat fetch_remote_specs dependency if remote? + + ensure_specs specs - options[:args] << '' if options[:args].empty? + specs.uniq.sort + end + + def gem_dependency args, version, prerelease # :nodoc: + args << '' if args.empty? - pattern = if options[:args].length == 1 and - options[:args].first =~ /\A\/(.*)\/(i)?\z/m then + pattern = if args.length == 1 and args.first =~ /\A\/(.*)\/(i)?\z/m then flags = $2 ? Regexp::IGNORECASE : nil Regexp.new $1, flags else - /\A#{Regexp.union(*options[:args])}/ + /\A#{Regexp.union(*args)}/ end - # TODO: deprecate for real damnit dependency = Gem::Deprecate.skip_during { - Gem::Dependency.new pattern, options[:version] + Gem::Dependency.new pattern, version } - dependency.prerelease = options[:prerelease] - specs = [] + dependency.prerelease = prerelease - specs.concat dependency.matching_specs if local? + dependency + end - if remote? and not options[:reverse_dependencies] then - fetcher = Gem::SpecFetcher.fetcher + def display_pipe specs # :nodoc: + specs.each do |spec| + unless spec.dependencies.empty? then + spec.dependencies.sort_by { |dep| dep.name }.each do |dep| + say "#{dep.name} --version '#{dep.requirement}'" + end + end + end + end - ss, _ = fetcher.spec_for_dependency dependency + def display_readable specs, reverse # :nodoc: + response = '' - ss.each { |s,o| specs << s } + specs.each do |spec| + response << print_dependencies(spec) + unless reverse[spec.full_name].empty? then + response << " Used by\n" + reverse[spec.full_name].each do |sp, dep| + response << " #{sp} (#{dep})\n" + end + end + response << "\n" end - if specs.empty? then - patterns = options[:args].join ',' - say "No gems found matching #{patterns} (#{options[:version]})" if - Gem.configuration.verbose + say response + end - terminate_interaction 1 - end + def execute + ensure_local_only_reverse_dependencies - specs = specs.uniq.sort + dependency = + gem_dependency options[:args], options[:version], options[:prerelease] - reverse = Hash.new { |h, k| h[k] = [] } + specs = fetch_specs dependency - if options[:reverse_dependencies] then - specs.each do |spec| - reverse[spec.full_name] = find_reverse_dependencies spec - end - end + reverse = reverse_dependencies specs if options[:pipe_format] then - specs.each do |spec| - unless spec.dependencies.empty? - spec.dependencies.sort_by { |dep| dep.name }.each do |dep| - say "#{dep.name} --version '#{dep.requirement}'" - end - end - end + display_pipe specs else - response = '' - - specs.each do |spec| - response << print_dependencies(spec) - unless reverse[spec.full_name].empty? then - response << " Used by\n" - reverse[spec.full_name].each do |sp, dep| - response << " #{sp} (#{dep})\n" - end - end - response << "\n" - end + display_readable specs, reverse + end + end - say response + def ensure_local_only_reverse_dependencies # :nodoc: + if options[:reverse_dependencies] and remote? and not local? then + alert_error 'Only reverse dependencies for local gems are supported.' + terminate_interaction 1 end end - def print_dependencies(spec, level = 0) + def ensure_specs specs # :nodoc: + return unless specs.empty? + + patterns = options[:args].join ',' + say "No gems found matching #{patterns} (#{options[:version]})" if + Gem.configuration.verbose + + terminate_interaction 1 + end + + def print_dependencies(spec, level = 0) # :nodoc: response = '' response << ' ' * level + "Gem #{spec.full_name}\n" unless spec.dependencies.empty? then @@ -131,10 +163,30 @@ class Gem::Commands::DependencyCommand < Gem::Command response end + def remote_specs dependency # :nodoc: + fetcher = Gem::SpecFetcher.fetcher + + ss, _ = fetcher.spec_for_dependency dependency + + ss.map { |s,o| s } + end + + def reverse_dependencies specs # :nodoc: + reverse = Hash.new { |h, k| h[k] = [] } + + return reverse unless options[:reverse_dependencies] + + specs.each do |spec| + reverse[spec.full_name] = find_reverse_dependencies spec + end + + reverse + end + ## # Returns an Array of [specification, dep] that are satisfied by +spec+. - def find_reverse_dependencies(spec) + def find_reverse_dependencies spec # :nodoc: result = [] Gem::Specification.each do |sp| diff --git a/lib/rubygems/commands/environment_command.rb b/lib/rubygems/commands/environment_command.rb index 40e71cf094..d32d12b757 100644 --- a/lib/rubygems/commands/environment_command.rb +++ b/lib/rubygems/commands/environment_command.rb @@ -21,6 +21,9 @@ class Gem::Commands::EnvironmentCommand < Gem::Command def description # :nodoc: <<-EOF +The environment command lets you query rubygems for its configuration for +use in shell scripts or as a debugging aid. + The RubyGems environment can be controlled through command line arguments, gemrc files, environment variables and built-in defaults. @@ -69,66 +72,83 @@ lib/rubygems/defaults/operating_system.rb def execute out = '' arg = options[:args][0] - case arg - when /^packageversion/ then - out << Gem::RubyGemsPackageVersion - when /^version/ then - out << Gem::VERSION - when /^gemdir/, /^gemhome/, /^home/, /^GEM_HOME/ then - out << Gem.dir - when /^gempath/, /^path/, /^GEM_PATH/ then - out << Gem.path.join(File::PATH_SEPARATOR) - when /^remotesources/ then - out << Gem.sources.to_a.join("\n") - when /^platform/ then - out << Gem.platforms.join(File::PATH_SEPARATOR) - when nil then - out = "RubyGems Environment:\n" - - out << " - RUBYGEMS VERSION: #{Gem::VERSION}\n" - - out << " - RUBY VERSION: #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}" - out << " patchlevel #{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL - out << ") [#{RUBY_PLATFORM}]\n" - - out << " - INSTALLATION DIRECTORY: #{Gem.dir}\n" - - out << " - RUBYGEMS PREFIX: #{Gem.prefix}\n" unless Gem.prefix.nil? - - out << " - RUBY EXECUTABLE: #{Gem.ruby}\n" - - out << " - EXECUTABLE DIRECTORY: #{Gem.bindir}\n" - - out << " - RUBYGEMS PLATFORMS:\n" - Gem.platforms.each do |platform| - out << " - #{platform}\n" + out << + case arg + when /^packageversion/ then + Gem::RubyGemsPackageVersion + when /^version/ then + Gem::VERSION + when /^gemdir/, /^gemhome/, /^home/, /^GEM_HOME/ then + Gem.dir + when /^gempath/, /^path/, /^GEM_PATH/ then + Gem.path.join(File::PATH_SEPARATOR) + when /^remotesources/ then + Gem.sources.to_a.join("\n") + when /^platform/ then + Gem.platforms.join(File::PATH_SEPARATOR) + when nil then + show_environment + else + raise Gem::CommandLineError, "Unknown environment option [#{arg}]" end + say out + true + end - out << " - GEM PATHS:\n" - out << " - #{Gem.dir}\n" + def add_path out, path + path.each do |component| + out << " - #{component}\n" + end + end - path = Gem.path.dup - path.delete Gem.dir - path.each do |p| - out << " - #{p}\n" - end + def show_environment # :nodoc: + out = "RubyGems Environment:\n" - out << " - GEM CONFIGURATION:\n" - Gem.configuration.each do |name, value| - value = value.gsub(/./, '*') if name == 'gemcutter_key' - out << " - #{name.inspect} => #{value.inspect}\n" - end + out << " - RUBYGEMS VERSION: #{Gem::VERSION}\n" - out << " - REMOTE SOURCES:\n" - Gem.sources.each do |s| - out << " - #{s}\n" - end + out << " - RUBY VERSION: #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}" + out << " patchlevel #{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL + out << ") [#{RUBY_PLATFORM}]\n" + + out << " - INSTALLATION DIRECTORY: #{Gem.dir}\n" + + out << " - RUBYGEMS PREFIX: #{Gem.prefix}\n" unless Gem.prefix.nil? + + out << " - RUBY EXECUTABLE: #{Gem.ruby}\n" + + out << " - EXECUTABLE DIRECTORY: #{Gem.bindir}\n" - else - raise Gem::CommandLineError, "Unknown environment option [#{arg}]" + out << " - SPEC CACHE DIRECTORY: #{Gem.spec_cache_dir}\n" + + out << " - RUBYGEMS PLATFORMS:\n" + Gem.platforms.each do |platform| + out << " - #{platform}\n" end - say out - true + + out << " - GEM PATHS:\n" + out << " - #{Gem.dir}\n" + + gem_path = Gem.path.dup + gem_path.delete Gem.dir + add_path out, gem_path + + out << " - GEM CONFIGURATION:\n" + Gem.configuration.each do |name, value| + value = value.gsub(/./, '*') if name == 'gemcutter_key' + out << " - #{name.inspect} => #{value.inspect}\n" + end + + out << " - REMOTE SOURCES:\n" + Gem.sources.each do |s| + out << " - #{s}\n" + end + + out << " - SHELL PATH:\n" + + shell_path = ENV['PATH'].split(File::PATH_SEPARATOR) + add_path out, shell_path + + out end end diff --git a/lib/rubygems/commands/fetch_command.rb b/lib/rubygems/commands/fetch_command.rb index ec021359b6..c57ab0089a 100644 --- a/lib/rubygems/commands/fetch_command.rb +++ b/lib/rubygems/commands/fetch_command.rb @@ -28,6 +28,16 @@ class Gem::Commands::FetchCommand < Gem::Command "--version '#{Gem::Requirement.default}'" end + def description # :nodoc: + <<-EOF +The fetch command fetches gem files that can be stored for later use or +unpacked to examine their contents. + +See the build command help for an example of unpacking a gem, modifying it, +then repackaging it. + EOF + end + def usage # :nodoc: "#{program_name} GEMNAME [GEMNAME ...]" end @@ -42,13 +52,15 @@ class Gem::Commands::FetchCommand < Gem::Command dep = Gem::Dependency.new gem_name, version dep.prerelease = options[:prerelease] - specs_and_sources, errors = Gem::SpecFetcher.fetcher.spec_for_dependency dep + 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 = specs_and_sources.sort_by { |s,| s.version }.first + spec, source = specs_and_sources.max_by { |s,| s.version } if spec.nil? then show_lookup_failure gem_name, version, errors, options[:domain] diff --git a/lib/rubygems/commands/help_command.rb b/lib/rubygems/commands/help_command.rb index 7f1fb486e0..ed7be903ac 100644 --- a/lib/rubygems/commands/help_command.rb +++ b/lib/rubygems/commands/help_command.rb @@ -46,6 +46,10 @@ Some examples of 'gem' usage. * Update all gems on your system: gem update + +* Update your local version of RubyGems + + gem update --system EOF PLATFORMS = <<-'EOF' @@ -55,8 +59,9 @@ your current platform by running `gem environment`. RubyGems matches platforms as follows: - * The CPU must match exactly, unless one of the platforms has - "universal" as the CPU. + * The CPU must match exactly unless one of the platforms has + "universal" as the CPU or the local CPU starts with "arm" and the gem's + CPU is exactly "arm" (for gems that support generic ARM architecture). * The OS must match exactly. * The versions must match exactly unless one of the versions is nil. @@ -66,11 +71,20 @@ you pass must match "#{cpu}-#{os}" or "#{cpu}-#{os}-#{version}". On mswin platforms, the version is the compiler version, not the OS version. (Ruby compiled with VC6 uses "60" as the compiler version, VC8 uses "80".) +For the ARM architecture, gems with a platform of "arm-linux" should run on a +reasonable set of ARM CPUs and not depend on instructions present on a limited +subset of the architecture. For example, the binary should run on platforms +armv5, armv6hf, armv6l, armv7, etc. If you use the "arm-linux" platform +please test your gem on a variety of ARM hardware before release to ensure it +functions correctly. + Example platforms: x86-freebsd # Any FreeBSD version on an x86 CPU universal-darwin-8 # Darwin 8 only gems that run on any CPU x86-mswin32-80 # Windows gems compiled with VC8 + armv7-linux # Gem complied for an ARMv7 CPU running linux + arm-linux # Gem compiled for any ARM CPU running linux When building platform gems, set the platform in the gem specification to Gem::Platform::CURRENT. This will correctly mark the gem with your ruby's @@ -80,6 +94,8 @@ platform. def initialize super 'help', "Provide help on the 'gem' command" + + @command_manager = Gem::CommandManager.instance end def arguments # :nodoc: @@ -96,46 +112,10 @@ platform. end def execute - command_manager = Gem::CommandManager.instance arg = options[:args][0] if begins? "commands", arg then - out = [] - out << "GEM commands are:" - out << nil - - margin_width = 4 - - desc_width = command_manager.command_names.map { |n| n.size }.max + 4 - - summary_width = 80 - margin_width - desc_width - wrap_indent = ' ' * (margin_width + desc_width) - format = "#{' ' * margin_width}%-#{desc_width}s%s" - - command_manager.command_names.each do |cmd_name| - command = command_manager[cmd_name] - - summary = - if command then - command.summary - else - "[No command found for #{cmd_name}, bug?]" - end - - summary = wrap(summary, summary_width).split "\n" - out << sprintf(format, cmd_name, summary.shift) - until summary.empty? do - out << "#{wrap_indent}#{summary.shift}" - end - end - - out << nil - out << "For help on a particular command, use 'gem help COMMAND'." - out << nil - out << "Commands may be abbreviated, so long as they are unambiguous." - out << "e.g. 'gem i rake' is short for 'gem install rake'." - - say out.join("\n") + show_commands elsif begins? "options", arg then say Gem::Command::HELP @@ -147,29 +127,79 @@ platform. say PLATFORMS elsif options[:help] then - command = command_manager[options[:help]] - if command - # help with provided command - command.invoke("--help") - else - alert_error "Unknown command #{options[:help]}. Try 'gem help commands'" - end + show_help elsif arg then - possibilities = command_manager.find_command_possibilities(arg.downcase) - if possibilities.size == 1 - command = command_manager[possibilities.first] - command.invoke("--help") - elsif possibilities.size > 1 - alert_warning "Ambiguous command #{arg} (#{possibilities.join(', ')})" - else - alert_warning "Unknown command #{arg}. Try gem help commands" - end + show_command_help arg else say Gem::Command::HELP end end + def show_commands # :nodoc: + out = [] + out << "GEM commands are:" + out << nil + + margin_width = 4 + + desc_width = @command_manager.command_names.map { |n| n.size }.max + 4 + + summary_width = 80 - margin_width - desc_width + wrap_indent = ' ' * (margin_width + desc_width) + format = "#{' ' * margin_width}%-#{desc_width}s%s" + + @command_manager.command_names.each do |cmd_name| + command = @command_manager[cmd_name] + + summary = + if command then + command.summary + else + "[No command found for #{cmd_name}]" + end + + summary = wrap(summary, summary_width).split "\n" + out << sprintf(format, cmd_name, summary.shift) + until summary.empty? do + out << "#{wrap_indent}#{summary.shift}" + end + end + + out << nil + out << "For help on a particular command, use 'gem help COMMAND'." + out << nil + out << "Commands may be abbreviated, so long as they are unambiguous." + out << "e.g. 'gem i rake' is short for 'gem install rake'." + + say out.join("\n") + end + + def show_command_help command_name # :nodoc: + command_name = command_name.downcase + + possibilities = @command_manager.find_command_possibilities command_name + + if possibilities.size == 1 then + command = @command_manager[possibilities.first] + command.invoke("--help") + elsif possibilities.size > 1 then + alert_warning "Ambiguous command #{command_name} (#{possibilities.join(', ')})" + else + alert_warning "Unknown command #{command_name}. Try: gem help commands" + end + end + + def show_help # :nodoc: + command = @command_manager[options[:help]] + if command then + # help with provided command + command.invoke("--help") + else + alert_error "Unknown command #{options[:help]}. Try 'gem help commands'" + end + end + end diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb index 0b58fa665e..f02b12906d 100644 --- a/lib/rubygems/commands/install_command.rb +++ b/lib/rubygems/commands/install_command.rb @@ -4,8 +4,6 @@ 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 @@ -40,6 +38,12 @@ class Gem::Commands::InstallCommand < Gem::Command o[:gemdeps] = v end + add_option(:"Install/Update", '--default', + 'Add the gem\'s full specification to', + 'specifications/default and extract only its bin') do |v,o| + o[:install_as_default] = v + end + @installed_specs = nil end @@ -109,7 +113,44 @@ to write the specification by hand. For example: "#{program_name} GEMNAME [GEMNAME ...] [options] -- --build-flags" end - def install_from_gemdeps(gf) + def check_install_dir # :nodoc: + if options[:install_dir] and options[:user_install] then + alert_error "Use --install-dir or --user-install but not both" + terminate_interaction 1 + end + end + + def check_version # :nodoc: + if options[:version] != Gem::Requirement.default and + get_all_gem_names.size > 1 then + alert_error "Can't use --version w/ multiple gems. Use name:ver instead." + terminate_interaction 1 + end + end + + def execute + if gf = options[:gemdeps] then + install_from_gemdeps gf + return + end + + @installed_specs = [] + + ENV.delete 'GEM_PATH' if options[:install_dir].nil? and RUBY_VERSION > '1.9' + + check_install_dir + check_version + + load_hooks + + exit_code = install_gems + + show_installed + + raise Gem::SystemExitException, exit_code + end + + def install_from_gemdeps gf # :nodoc: require 'rubygems/request_set' rs = Gem::RequestSet.new rs.load_gemdeps gf @@ -131,51 +172,26 @@ to write the specification by hand. For example: raise Gem::SystemExitException, 0 end - def execute - if gf = options[:gemdeps] then - install_from_gemdeps gf - return - end + def install_gem name, version # :nodoc: + return if options[:conservative] and + not Gem::Dependency.new(name, version).matching_specs.empty? - @installed_specs = [] + inst = Gem::DependencyInstaller.new options + inst.install name, Gem::Requirement.create(version) - ENV.delete 'GEM_PATH' if options[:install_dir].nil? and RUBY_VERSION > '1.9' + @installed_specs.push(*inst.installed_gems) - if options[:install_dir] and options[:user_install] - alert_error "Use --install-dir or --user-install but not both" - terminate_interaction 1 - end + show_install_errors inst.errors + end + def install_gems # :nodoc: exit_code = 0 - 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, gem_version).matching_specs.empty? - - inst = Gem::DependencyInstaller.new options - inst.install gem_name, Gem::Requirement.create(gem_version) - - @installed_specs.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 + install_gem gem_name, gem_version rescue Gem::InstallError => e alert_error "Error installing #{gem_name}:\n\t#{e.message}" exit_code |= 1 @@ -186,12 +202,38 @@ to write the specification by hand. For example: end end - unless @installed_specs.empty? then - gems = @installed_specs.length == 1 ? 'gem' : 'gems' - say "#{@installed_specs.length} #{gems} installed" + exit_code + end + + ## + # Loads post-install hooks + + def load_hooks # :nodoc: + if options[:install_as_default] + require 'rubygems/install_default_message' + else + require 'rubygems/install_message' end + require 'rubygems/rdoc' + end - raise Gem::SystemExitException, exit_code + def show_install_errors errors # :nodoc: + return unless errors + + errors.each do |x| + return unless Gem::SourceFetchProblem === x + + msg = "Unable to pull data from '#{x.source.uri}': #{x.error.message}" + + alert_warning msg + end + end + + def show_installed # :nodoc: + return if @installed_specs.empty? + + gems = @installed_specs.length == 1 ? 'gem' : 'gems' + say "#{@installed_specs.length} #{gems} installed" end end diff --git a/lib/rubygems/commands/list_command.rb b/lib/rubygems/commands/list_command.rb index f3e5da9551..0d15950475 100644 --- a/lib/rubygems/commands/list_command.rb +++ b/lib/rubygems/commands/list_command.rb @@ -8,7 +8,7 @@ require 'rubygems/commands/query_command' class Gem::Commands::ListCommand < Gem::Commands::QueryCommand def initialize - super 'list', 'Display gems whose name starts with STRING' + super 'list', 'Display local gems whose name starts with STRING' remove_option('--name-matches') end @@ -21,6 +21,17 @@ class Gem::Commands::ListCommand < Gem::Commands::QueryCommand "--local --no-details" end + def description # :nodoc: + <<-EOF +The list command is used to view the gems you have installed locally. + +The --details option displays additional details including the summary, the +homepage, the author, the locations of different versions of the gem. + +To search for remote gems use the search command. + EOF + end + def usage # :nodoc: "#{program_name} [STRING]" end diff --git a/lib/rubygems/commands/mirror_command.rb b/lib/rubygems/commands/mirror_command.rb index 0f98077cbd..75419c857a 100644 --- a/lib/rubygems/commands/mirror_command.rb +++ b/lib/rubygems/commands/mirror_command.rb @@ -10,6 +10,12 @@ class Gem::Commands::MirrorCommand < Gem::Command end end + def description # :nodoc: + <<-EOF +The mirror command has been moved to the rubygems-mirror gem. + EOF + end + def execute alert_error "Install the rubygems-mirror gem for the mirror command" end diff --git a/lib/rubygems/commands/outdated_command.rb b/lib/rubygems/commands/outdated_command.rb index 887faab0a2..f51bc5e93f 100644 --- a/lib/rubygems/commands/outdated_command.rb +++ b/lib/rubygems/commands/outdated_command.rb @@ -15,19 +15,18 @@ class Gem::Commands::OutdatedCommand < Gem::Command add_platform_option end - def execute - 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.spec_for_dependency dep - - next if remotes.empty? - - remotes.sort! { |a,b| a[0].version <=> b[0].version } + def description # :nodoc: + <<-EOF +The outdated command lists gems you way wish to upgrade to a newer version. - highest = remotes.last.first +You can check for dependency mismatches using the dependency command and +update the gems with the update or install commands. + EOF + end - say "#{local.name} (#{local.version} < #{highest.version})" + def execute + Gem::Specification.outdated_and_latest_version.each do |spec, remote_version| + say "#{spec.name} (#{spec.version} < #{remote_version})" end end end diff --git a/lib/rubygems/commands/owner_command.rb b/lib/rubygems/commands/owner_command.rb index 11e6e026fd..13b8793021 100644 --- a/lib/rubygems/commands/owner_command.rb +++ b/lib/rubygems/commands/owner_command.rb @@ -7,7 +7,14 @@ class Gem::Commands::OwnerCommand < Gem::Command include Gem::GemcutterUtilities def description # :nodoc: - 'Manage gem owners on RubyGems.org.' + <<-EOF +The owner command lets you add and remove owners of a gem on a push +server (the default is https://rubygems.org). + +The owner of a gem has the permission to push new versions, yank existing +versions or edit the HTML page of the gem. Be careful of who you give push +permission to. + EOF end def arguments # :nodoc: @@ -19,7 +26,7 @@ class Gem::Commands::OwnerCommand < Gem::Command end def initialize - super 'owner', description + super 'owner', 'Manage gem owners of a gem on the push server' add_proxy_option add_key_option defaults.merge! :add => [], :remove => [] @@ -31,9 +38,15 @@ class Gem::Commands::OwnerCommand < Gem::Command add_option '-r', '--remove EMAIL', 'Remove an owner' do |value, options| options[:remove] << value end + + add_option '-h', '--host HOST', 'Use another gemcutter-compatible host' do |value, options| + options[:host] = value + end end def execute + @host = options[:host] + sign_in name = get_one_gem_name diff --git a/lib/rubygems/commands/pristine_command.rb b/lib/rubygems/commands/pristine_command.rb index 8d479211ac..3f3bca45be 100644 --- a/lib/rubygems/commands/pristine_command.rb +++ b/lib/rubygems/commands/pristine_command.rb @@ -31,6 +31,12 @@ class Gem::Commands::PristineCommand < Gem::Command options[:only_executables] = value end + add_option('-E', '--[no-]env-shebang', + 'Rewrite executables with a shebang', + 'of /usr/bin/env') do |value, options| + options[:env_shebang] = value + end + add_version_option('restore to', 'pristine condition') end @@ -105,16 +111,21 @@ with an extension. Gem::RemoteFetcher.fetcher.download_to_cache dep end - # TODO use installer options - install_defaults = Gem::ConfigFile::PLATFORM_DEFAULTS['install'] - installer_env_shebang = install_defaults.to_s['--env-shebang'] + env_shebang = + if options.include? :env_shebang then + options[:env_shebang] + else + install_defaults = Gem::ConfigFile::PLATFORM_DEFAULTS['install'] + install_defaults.to_s['--env-shebang'] + end installer = Gem::Installer.new(gem, :wrappers => true, :force => true, :install_dir => spec.base_dir, - :env_shebang => installer_env_shebang, + :env_shebang => env_shebang, :build_args => spec.build_args) + if options[:only_executables] then installer.generate_bin else diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb index fccad206fa..b90be7bd10 100644 --- a/lib/rubygems/commands/push_command.rb +++ b/lib/rubygems/commands/push_command.rb @@ -8,7 +8,13 @@ class Gem::Commands::PushCommand < Gem::Command include Gem::GemcutterUtilities def description # :nodoc: - 'Push a gem up to RubyGems.org' + <<-EOF +The push command uploads a gem to the push server (the default is +https://rubygems.org) and adds it to the index. + +The gem can be removed from the index (but only the index) using the yank +command. For further discussion see the help for the yank command. + EOF end def arguments # :nodoc: @@ -20,7 +26,7 @@ class Gem::Commands::PushCommand < Gem::Command end def initialize - super 'push', description, :host => self.host + super 'push', 'Push a gem up to the gem server', :host => self.host add_proxy_option add_key_option diff --git a/lib/rubygems/commands/query_command.rb b/lib/rubygems/commands/query_command.rb index 05b214bb63..c9c3014975 100644 --- a/lib/rubygems/commands/query_command.rb +++ b/lib/rubygems/commands/query_command.rb @@ -61,6 +61,15 @@ class Gem::Commands::QueryCommand < Gem::Command "--local --name-matches // --no-details --versions --no-installed" end + def description # :nodoc: + <<-EOF +The query command is the basis for the list and search commands. + +You should really use the list and search commands instead. This command +is too hard to use. + EOF + end + def execute exit_code = 0 diff --git a/lib/rubygems/commands/rdoc_command.rb b/lib/rubygems/commands/rdoc_command.rb index df00f3a5df..86597f99a6 100644 --- a/lib/rubygems/commands/rdoc_command.rb +++ b/lib/rubygems/commands/rdoc_command.rb @@ -45,8 +45,12 @@ class Gem::Commands::RdocCommand < Gem::Command def description # :nodoc: <<-DESC -The rdoc command builds RDoc and RI documentation for installed gems. Use ---overwrite to force rebuilding of documentation. +The rdoc command builds documentation for installed gems. By default +only documentation is built using rdoc, but additional types of +documentation may be built through rubygems plugins and the +Gem.post_installs hook. + +Use --overwrite to force rebuilding of documentation. DESC end diff --git a/lib/rubygems/commands/search_command.rb b/lib/rubygems/commands/search_command.rb index c125715fe2..5bc9650672 100644 --- a/lib/rubygems/commands/search_command.rb +++ b/lib/rubygems/commands/search_command.rb @@ -4,7 +4,7 @@ require 'rubygems/commands/query_command' class Gem::Commands::SearchCommand < Gem::Commands::QueryCommand def initialize - super 'search', 'Display all gems whose name contains STRING' + super 'search', 'Display remote gems whose name contains STRING' remove_option '--name-matches' @@ -19,6 +19,19 @@ class Gem::Commands::SearchCommand < Gem::Commands::QueryCommand "--remote --no-details" end + def description # :nodoc: + <<-EOF +The search command displays remote gems whose name contains the given +string. + +The --details option displays additional details from the gem but will +take a little longer to complete as it must download the information +individually from the index. + +To list local gems use the list command. + EOF + end + def usage # :nodoc: "#{program_name} [STRING]" end diff --git a/lib/rubygems/commands/sources_command.rb b/lib/rubygems/commands/sources_command.rb index f4cc3e57ae..60d96c5828 100644 --- a/lib/rubygems/commands/sources_command.rb +++ b/lib/rubygems/commands/sources_command.rb @@ -37,103 +37,165 @@ class Gem::Commands::SourcesCommand < Gem::Command add_proxy_option end - def defaults_str - '--list' - end - - def execute - options[:list] = !(options[:add] || - options[:clear_all] || - options[:remove] || - options[:update]) + def add_source source_uri # :nodoc: + check_rubygems_https source_uri - if options[:clear_all] then - path = File.join Gem.user_home, '.gem', 'specs' - FileUtils.rm_rf path + source = Gem::Source.new source_uri - unless File.exist? path then - say "*** Removed specs cache ***" + begin + if Gem.sources.include? source_uri then + say "source #{source_uri} already present in the cache" else - unless File.writable? path then - say "*** Unable to remove source cache (write protected) ***" - else - say "*** Unable to remove source cache ***" - end + source.load_specs :released + Gem.sources << source + Gem.configuration.write - terminate_interaction 1 + say "#{source_uri} added to sources" end + rescue URI::Error, ArgumentError + say "#{source_uri} is not a URI" + terminate_interaction 1 + rescue Gem::RemoteFetcher::FetchError => e + say "Error fetching #{source_uri}:\n\t#{e.message}" + terminate_interaction 1 end + end - if source_uri = options[:add] then - uri = URI source_uri + def check_rubygems_https source_uri # :nodoc: + uri = URI source_uri - if uri.scheme and uri.scheme.downcase == 'http' and - uri.host.downcase == 'rubygems.org' then - question = <<-QUESTION.chomp + if uri.scheme and uri.scheme.downcase == 'http' and + uri.host.downcase == 'rubygems.org' then + question = <<-QUESTION.chomp https://rubygems.org is recommended for security over #{uri} Do you want to add this insecure source? - QUESTION + QUESTION - terminate_interaction 1 unless ask_yes_no question - end - - source = Gem::Source.new source_uri - - begin - 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" - end - rescue URI::Error, ArgumentError - say "#{source_uri} is not a URI" - terminate_interaction 1 - rescue Gem::RemoteFetcher::FetchError => e - say "Error fetching #{source_uri}:\n\t#{e.message}" - terminate_interaction 1 - end + terminate_interaction 1 unless ask_yes_no question end + end - if options[:remove] then - source_uri = options[:remove] + def clear_all # :nodoc: + path = Gem.spec_cache_dir + FileUtils.rm_rf path - unless Gem.sources.include? source_uri then - say "source #{source_uri} not present in cache" + unless File.exist? path then + say "*** Removed specs cache ***" + else + unless File.writable? path then + say "*** Unable to remove source cache (write protected) ***" else - Gem.sources.delete source_uri - Gem.configuration.write - - say "#{source_uri} removed from sources" + say "*** Unable to remove source cache ***" end + + terminate_interaction 1 end + end - if options[:update] then - Gem.sources.each_source do |src| - src.load_specs :released - src.load_specs :latest - end + def defaults_str # :nodoc: + '--list' + end + + def description # :nodoc: + <<-EOF +RubyGems fetches gems from the sources you have configured (stored in your +~/.gemrc). + +The default source is https://rubygems.org, but you may have older sources +configured. This guide will help you update your sources or configure +yourself to use your own gem server. + +Without any arguments the sources lists your currently configured sources: + + $ gem sources + *** CURRENT SOURCES *** + + https://rubygems.org + +This may list multiple sources or non-rubygems sources. You probably +configured them before or have an old `~/.gemrc`. If you have sources you +do not recognize you should remove them. + +RubyGems has been configured to serve gems via the following URLs through +its history: - say "source cache successfully updated" +* http://gems.rubyforge.org (RubyGems 1.3.6 and earlier) +* http://rubygems.org (RubyGems 1.3.7 through 1.8.25) +* https://rubygems.org (RubyGems 2.0.1 and newer) + +Since all of these sources point to the same set of gems you only need one +of them in your list. https://rubygems.org is recommended as it brings the +protections of an SSL connection to gem downloads. + +To add a source use the --add argument: + + $ gem sources --add https://rubygems.org + https://rubygems.org added to sources + +RubyGems will check to see if gems can be installed from the source given +before it is added. + +To remove a source use the --remove argument: + + $ gem sources --remove http://rubygems.org + http://rubygems.org removed from sources + + EOF + end + + def list # :nodoc: + say "*** CURRENT SOURCES ***" + say + + Gem.sources.each do |src| + say src end + end - if options[:list] then - say "*** CURRENT SOURCES ***" - say + def list? # :nodoc: + !(options[:list] || + options[:add] || + options[:clear_all] || + options[:remove] || + options[:update]) + end - Gem.sources.each do |src| - say src - end + def execute + clear_all if options[:clear_all] + + source_uri = options[:add] + add_source source_uri if source_uri + + source_uri = options[:remove] + remove_source source_uri if source_uri + + update if options[:update] + + list if list? + end + + def remove_source source_uri # :nodoc: + unless Gem.sources.include? source_uri then + say "source #{source_uri} not present in cache" + else + Gem.sources.delete source_uri + Gem.configuration.write + + say "#{source_uri} removed from sources" end end - private + def update # :nodoc: + Gem.sources.each_source do |src| + src.load_specs :released + src.load_specs :latest + end + + say "source cache successfully updated" + end - def remove_cache_file(desc, path) + def remove_cache_file desc, path # :nodoc: FileUtils.rm_rf path if not File.exist?(path) then diff --git a/lib/rubygems/commands/specification_command.rb b/lib/rubygems/commands/specification_command.rb index b40dfd5f3c..d96c8b8627 100644 --- a/lib/rubygems/commands/specification_command.rb +++ b/lib/rubygems/commands/specification_command.rb @@ -50,6 +50,22 @@ FIELD name of gemspec field to show "--local --version '#{Gem::Requirement.default}' --yaml" end + def description # :nodoc: + <<-EOF +The specification command allows you to extract the specification from +a gem for examination. + +The specification can be output in YAML, ruby or Marshal formats. + +Specific fields in the specification can be extracted in YAML format: + + $ gem spec rake summary + --- Ruby based make-like utility. + ... + + EOF + end + def usage # :nodoc: "#{program_name} [GEMFILE] [FIELD]" end diff --git a/lib/rubygems/commands/stale_command.rb b/lib/rubygems/commands/stale_command.rb index 36c517e27c..0ef0755960 100644 --- a/lib/rubygems/commands/stale_command.rb +++ b/lib/rubygems/commands/stale_command.rb @@ -5,6 +5,16 @@ class Gem::Commands::StaleCommand < Gem::Command super('stale', 'List gems along with access times') end + def description # :nodoc: + <<-EOF +The stale command lists the latest access time for all the files in your +installed gems. + +You can use this command to discover gems and gem versions you are no +longer using. + EOF + end + def usage # :nodoc: "#{program_name}" end diff --git a/lib/rubygems/commands/uninstall_command.rb b/lib/rubygems/commands/uninstall_command.rb index 56aa8ee57f..8e6af2ba65 100644 --- a/lib/rubygems/commands/uninstall_command.rb +++ b/lib/rubygems/commands/uninstall_command.rb @@ -1,6 +1,7 @@ require 'rubygems/command' require 'rubygems/version_option' require 'rubygems/uninstaller' +require 'fileutils' ## # Gem uninstaller command line tool @@ -14,7 +15,7 @@ class Gem::Commands::UninstallCommand < Gem::Command def initialize super 'uninstall', 'Uninstall gems from the local repository', :version => Gem::Requirement.default, :user_install => true, - :check_dev => false + :install_dir => Gem.dir, :check_dev => false add_option('-a', '--[no-]all', 'Uninstall all matching versions' @@ -67,6 +68,12 @@ class Gem::Commands::UninstallCommand < Gem::Command options[:force] = value end + add_option('--[no-]abort-on-dependent', + 'Prevent uninstalling gems that are', + 'depended on by other gems.') do |value, options| + options[:abort_on_dependent] = value + end + add_version_option add_platform_option end @@ -81,13 +88,49 @@ class Gem::Commands::UninstallCommand < Gem::Command "--user-install" end + def description # :nodoc: + <<-EOF +The uninstall command removes a previously installed gem. + +RubyGems will ask for confirmation if you are attempting to uninstall a gem +that is a dependency of an existing gem. You can use the +--ignore-dependencies option to skip this check. + EOF + end + def usage # :nodoc: "#{program_name} GEMNAME [GEMNAME ...]" end def execute - # REFACTOR: stolen from cleanup_command + if options[:all] and not options[:args].empty? then + alert_error 'Gem names and --all may not be used together' + terminate_interaction 1 + elsif options[:all] then + uninstall_all + else + uninstall_specific + end + end + + def uninstall_all + _, specs = Gem::Specification.partition { |spec| spec.default_gem? } + + specs.each do |spec| + options[:version] = spec.version + + begin + Gem::Uninstaller.new(spec.name, options).uninstall + rescue Gem::InstallError + end + end + + alert "Uninstalled all gems in #{options[:install_dir]}" + end + + def uninstall_specific 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 diff --git a/lib/rubygems/commands/unpack_command.rb b/lib/rubygems/commands/unpack_command.rb index 7eefd32a6e..e60e7d90fd 100644 --- a/lib/rubygems/commands/unpack_command.rb +++ b/lib/rubygems/commands/unpack_command.rb @@ -34,6 +34,24 @@ class Gem::Commands::UnpackCommand < Gem::Command "--version '#{Gem::Requirement.default}'" end + def description + <<-EOF +The unpack command allows you to examine the contents of a gem or modify +them to help diagnose a bug. + +You can add the contents of the unpacked gem to the load path using the +RUBYLIB environment variable or -I: + + $ gem unpack my_gem + Unpacked gem: '.../my_gem-1.0' + [edit my_gem-1.0/lib/my_gem.rb] + $ ruby -Imy_gem-1.0/lib -S other_program + +You can repackage an unpacked gem using the build command. See the build +command help for an example. + EOF + end + def usage # :nodoc: "#{program_name} GEMNAME" end diff --git a/lib/rubygems/commands/update_command.rb b/lib/rubygems/commands/update_command.rb index a31de0071a..77bf5edb45 100644 --- a/lib/rubygems/commands/update_command.rb +++ b/lib/rubygems/commands/update_command.rb @@ -52,27 +52,46 @@ class Gem::Commands::UpdateCommand < Gem::Command "--document --no-force --install-dir #{Gem.dir}" end + def description # :nodoc: + <<-EOF +The update command will update your gems to the latest version. + +The update comamnd does not remove the previous version. Use the cleanup +command to remove old versions. + EOF + end + def usage # :nodoc: "#{program_name} GEMNAME [GEMNAME ...]" end + def check_latest_rubygems version # :nodoc: + if Gem.rubygems_version == version then + say "Latest version currently installed. Aborting." + terminate_interaction + end + + options[:user_install] = false + end + + def check_update_arguments # :nodoc: + unless options[:args].empty? then + alert_error "Gem names are not allowed with the --system option" + terminate_interaction 1 + end + end + def execute hig = {} if options[:system] then update_rubygems return - else - say "Updating installed gems" + end - hig = {} # highest installed gems + say "Updating installed gems" - Gem::Specification.each do |spec| - if hig[spec.name].nil? or hig[spec.name].version < spec.version then - hig[spec.name] = spec - end - end - end + hig = highest_installed_gems gems_to_update = which_to_update hig, options[:args].uniq @@ -85,51 +104,65 @@ class Gem::Commands::UpdateCommand < Gem::Command end end - def update_gem name, version = Gem::Requirement.default - return if @updated.any? { |spec| spec.name == name } + def fetch_remote_gems spec # :nodoc: + dependency = Gem::Dependency.new spec.name, "> #{spec.version}" + dependency.prerelease = options[:prerelease] - @installer ||= Gem::DependencyInstaller.new options + fetcher = Gem::SpecFetcher.fetcher - success = false + spec_tuples, _ = fetcher.search_for_dependency dependency - say "Updating #{name}" - begin - @installer.install name, Gem::Requirement.new(version) - success = true - rescue Gem::InstallError => e - alert_error "Error installing #{name}:\n\t#{e.message}" - success = false - end + spec_tuples + end - @installer.installed_gems.each do |spec| - @updated << spec + def highest_installed_gems # :nodoc: + hig = {} # highest installed gems + + Gem::Specification.each do |spec| + if hig[spec.name].nil? or hig[spec.name].version < spec.version then + hig[spec.name] = spec + end end + + hig end - def update_gems gems_to_update - gems_to_update.uniq.sort.each do |(name, version)| - update_gem name, version + def highest_remote_version spec # :nodoc: + spec_tuples = fetch_remote_gems spec + + matching_gems = spec_tuples.select do |g,_| + g.name == spec.name and g.match_platform? end - @updated + highest_remote_gem = matching_gems.sort_by { |g,_| g.version }.last + + highest_remote_gem ||= [Gem::NameTuple.null] + + highest_remote_gem.first.version end - ## - # Update RubyGems software to the latest version. + def install_rubygems version # :nodoc: + args = update_rubygems_arguments - def update_rubygems - unless options[:args].empty? then - alert_error "Gem names are not allowed with the --system option" - terminate_interaction 1 - end + update_dir = File.join Gem.dir, 'gems', "rubygems-update-#{version}" - options[:user_install] = false + Dir.chdir update_dir do + say "Installing RubyGems #{version}" - # TODO: rename version and other variable name conflicts - # TODO: get rid of all this indirection on name and other BS + # Make sure old rubygems isn't loaded + old = ENV["RUBYOPT"] + ENV.delete("RUBYOPT") if old + installed = system Gem.ruby, 'setup.rb', *args + say "RubyGems system software updated" if installed + ENV["RUBYOPT"] = old if old + end + end + def rubygems_target_version version = options[:system] - if version == true then + update_latest = version == true + + if update_latest then version = Gem::Version.new Gem::VERSION requirement = Gem::Requirement.new ">= #{Gem::VERSION}" else @@ -146,46 +179,72 @@ class Gem::Commands::UpdateCommand < Gem::Command } gems_to_update = which_to_update hig, options[:args], :system - name, up_ver = gems_to_update.first - current_ver = Gem.rubygems_version + _, up_ver = gems_to_update.first - target = if options[:system] == true then + target = if update_latest then up_ver else version end - if current_ver == target then - # if options[:system] != true and version == current_ver then - say "Latest version currently installed. Aborting." - terminate_interaction + return target, requirement + end + + def update_gem name, version = Gem::Requirement.default + return if @updated.any? { |spec| spec.name == name } + + @installer ||= Gem::DependencyInstaller.new options + + success = false + + say "Updating #{name}" + begin + @installer.install name, Gem::Requirement.new(version) + success = true + rescue Gem::InstallError => e + alert_error "Error installing #{name}:\n\t#{e.message}" + success = false end - update_gem name, target + @installer.installed_gems.each do |spec| + @updated << spec + end + end + + def update_gems gems_to_update + gems_to_update.uniq.sort.each do |(name, version)| + update_gem name, version + end + + @updated + end + + ## + # Update RubyGems software to the latest version. + + def update_rubygems + check_update_arguments + + version, requirement = rubygems_target_version + + check_latest_rubygems version + + update_gem 'rubygems-update', version installed_gems = Gem::Specification.find_all_by_name 'rubygems-update', requirement version = installed_gems.last.version + install_rubygems version + end + + def update_rubygems_arguments # :nodoc: args = [] args << '--prefix' << Gem.prefix if Gem.prefix # 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}" - - Dir.chdir update_dir do - say "Installing RubyGems #{version}" - setup_cmd = "#{Gem.ruby} setup.rb #{args.join ' '}" - - # Make sure old rubygems isn't loaded - old = ENV["RUBYOPT"] - ENV.delete("RUBYOPT") if old - installed = system setup_cmd - say "RubyGems system software updated" if installed - ENV["RUBYOPT"] = old if old - end + args end def which_to_update highest_installed_gems, gem_names, system = false @@ -195,21 +254,7 @@ class Gem::Commands::UpdateCommand < Gem::Command next if not gem_names.empty? and 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.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 { |g,_| g.version }.last - - highest_remote_gem ||= [Gem::NameTuple.null] - highest_remote_ver = highest_remote_gem.first.version + highest_remote_ver = highest_remote_version l_spec 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/which_command.rb b/lib/rubygems/commands/which_command.rb index 6495278a87..99b9085b2b 100644 --- a/lib/rubygems/commands/which_command.rb +++ b/lib/rubygems/commands/which_command.rb @@ -23,6 +23,17 @@ class Gem::Commands::WhichCommand < Gem::Command "--no-gems-first --no-all" end + def description # :nodoc: + <<-EOF +The which command is like the shell which command and shows you where +the file you wish to require lives. + +You can use the which command to help determine why you are requiring a +version you did not expect or to look at the content of a file you are +requiring to see why it does not behave as you expect. + EOF + end + def execute found = false diff --git a/lib/rubygems/commands/yank_command.rb b/lib/rubygems/commands/yank_command.rb index df4142d395..2285bb4017 100644 --- a/lib/rubygems/commands/yank_command.rb +++ b/lib/rubygems/commands/yank_command.rb @@ -9,7 +9,21 @@ class Gem::Commands::YankCommand < Gem::Command include Gem::GemcutterUtilities def description # :nodoc: - 'Remove a specific gem version release from RubyGems.org' + <<-EOF +The yank command removes a gem you pushed to a server from the server's +index. + +Note that if you push a gem to rubygems.org the yank command does not +prevent other people from downloading the gem via the download link. + +Once you have pushed a gem several downloads will happen automatically +via the webhooks. If you accidentally pushed passwords or other sensitive +data you will need to change them immediately and yank your gem. + +If you are yanking a gem due to intellectual property reasons contact +http://help.rubygems.org for permanant removal. Be sure to mention this +as the reason for the removal request. + EOF end def arguments # :nodoc: @@ -21,7 +35,7 @@ class Gem::Commands::YankCommand < Gem::Command end def initialize - super 'yank', description + super 'yank', 'Remove a pushed gem from the index' add_version_option("remove") add_platform_option("remove") diff --git a/lib/rubygems/config_file.rb b/lib/rubygems/config_file.rb index 244e845e6f..d0699dcb29 100644 --- a/lib/rubygems/config_file.rb +++ b/lib/rubygems/config_file.rb @@ -140,6 +140,11 @@ class Gem::ConfigFile attr_reader :ssl_ca_cert + ## + # Path name of directory or file of openssl client certificate, used for remote https connection with client authentication + + attr_reader :ssl_client_cert + ## # Create the config file object. +args+ is the list of arguments # from the command line. @@ -210,6 +215,7 @@ class Gem::ConfigFile @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 + @ssl_client_cert = @hash[:ssl_client_cert] if @hash.key? :ssl_client_cert @api_keys = nil @rubygems_api_key = nil @@ -246,6 +252,10 @@ Your gem push credentials file located at: has file permissions of 0#{existing_permissions.to_s 8} but 0600 is required. +To fix this error run: + +\tchmod 0600 #{credentials_path} + You should reset your credentials at: \thttps://rubygems.org/profile/edit @@ -309,6 +319,9 @@ if you believe they were disclosed to a third party. @rubygems_api_key = api_key end + YAMLErrors = [ArgumentError] + YAMLErrors << Psych::SyntaxError if defined?(Psych::SyntaxError) + def load_file(filename) Gem.load_yaml @@ -321,8 +334,8 @@ if you believe they were disclosed to a third party. return {} end return content - rescue ArgumentError - warn "Failed to load #{filename}" + rescue *YAMLErrors => e + warn "Failed to load #{filename}, #{e.to_s}" rescue Errno::EACCES warn "Failed to load #{filename} due to permissions problem." end diff --git a/lib/rubygems/core_ext/kernel_require.rb b/lib/rubygems/core_ext/kernel_require.rb index f4f7fc8393..0416644920 100755 --- a/lib/rubygems/core_ext/kernel_require.rb +++ b/lib/rubygems/core_ext/kernel_require.rb @@ -48,7 +48,12 @@ module Kernel # normal require handle loading a gem from the rescue below. if Gem::Specification.unresolved_deps.empty? then - return gem_original_require(path) + begin + RUBYGEMS_ACTIVATION_MONITOR.exit + return gem_original_require(path) + ensure + RUBYGEMS_ACTIVATION_MONITOR.enter + end end # If +path+ is for a gem that has already been loaded, don't @@ -57,11 +62,16 @@ module Kernel #-- # TODO request access to the C implementation of this to speed up RubyGems - spec = Gem::Specification.find { |s| + spec = Gem::Specification.stubs.find { |s| s.activated? and s.contains_requirable_file? path } - return gem_original_require(path) if spec + begin + RUBYGEMS_ACTIVATION_MONITOR.exit + return gem_original_require(path) + ensure + RUBYGEMS_ACTIVATION_MONITOR.enter + end if spec # Attempt to find +path+ in any unresolved gems... @@ -109,11 +119,21 @@ module Kernel valid.activate end - gem_original_require path + begin + RUBYGEMS_ACTIVATION_MONITOR.exit + return gem_original_require(path) + ensure + RUBYGEMS_ACTIVATION_MONITOR.enter + 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) + begin + RUBYGEMS_ACTIVATION_MONITOR.exit + return gem_original_require(path) + ensure + RUBYGEMS_ACTIVATION_MONITOR.enter + end end raise load_error diff --git a/lib/rubygems/defaults.rb b/lib/rubygems/defaults.rb index cc8dc722fc..591580b7da 100644 --- a/lib/rubygems/defaults.rb +++ b/lib/rubygems/defaults.rb @@ -14,6 +14,14 @@ module Gem %w[https://rubygems.org/] end + ## + # Default spec directory path to be used if an alternate value is not + # specified in the environment + + def self.default_spec_cache_dir + File.join Gem.user_home, '.gem', 'specs' + end + ## # Default home directory path to be used if an alternate value is not # specified in the environment @@ -126,4 +134,11 @@ module Gem def self.default_cert_path File.join Gem.user_home, ".gem", "gem-public_cert.pem" end + + ## + # Whether to expect full paths in default gems - true for non-MRI + # ruby implementations + def self.default_gems_use_full_paths? + ruby_engine != 'ruby' + end end diff --git a/lib/rubygems/dependency.rb b/lib/rubygems/dependency.rb index 1e3cc168a8..a96d67c3e5 100644 --- a/lib/rubygems/dependency.rb +++ b/lib/rubygems/dependency.rb @@ -203,6 +203,8 @@ class Gem::Dependency requirement.satisfied_by? version end + alias === =~ + # DOC: this method needs either documented or :nodoc'd def match? obj, version=nil @@ -250,10 +252,10 @@ class Gem::Dependency # DOC: this method needs either documented or :nodoc'd def matching_specs platform_only = false - matches = Gem::Specification.find_all { |spec| + matches = Gem::Specification.stubs.find_all { |spec| self.name === spec.name and # TODO: == instead of === requirement.satisfied_by? spec.version - } + }.map(&:to_spec) if platform_only matches.reject! { |spec| diff --git a/lib/rubygems/dependency_installer.rb b/lib/rubygems/dependency_installer.rb index 6f19a310f7..e7c489a759 100644 --- a/lib/rubygems/dependency_installer.rb +++ b/lib/rubygems/dependency_installer.rb @@ -1,11 +1,11 @@ require 'rubygems' require 'rubygems/dependency_list' +require 'rubygems/dependency_resolver' 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/source' require 'rubygems/available_set' ## @@ -15,15 +15,7 @@ class Gem::DependencyInstaller include Gem::UserInteraction - attr_reader :gems_to_install - attr_reader :installed_gems - - ## - # Documentation types. For use by the Gem.done_installing hook - - attr_reader :document - - DEFAULT_OPTIONS = { + DEFAULT_OPTIONS = { # :nodoc: :env_shebang => false, :document => %w[ri], :domain => :both, # HACK dup @@ -35,8 +27,30 @@ class Gem::DependencyInstaller :wrappers => true, :build_args => nil, :build_docs_in_background => false, + :install_as_default => false }.freeze + ## + # Documentation types. For use by the Gem.done_installing hook + + attr_reader :document + + ## + # Errors from SpecFetcher while searching for remote specifications + + attr_reader :errors + + ## + #-- + # TODO remove, no longer used + + attr_reader :gems_to_install # :nodoc: + + ## + # List of gems installed by #install in alphabetic order + + attr_reader :installed_gems + ## # Creates a new installer instance. # @@ -56,7 +70,8 @@ class Gem::DependencyInstaller # :wrappers:: See Gem::Installer::new # :build_args:: See Gem::Installer::new - def initialize(options = {}) + def initialize options = {} + @only_install_dir = !!options[:install_dir] @install_dir = options[:install_dir] || Gem.dir if options[:install_dir] then @@ -82,6 +97,7 @@ class Gem::DependencyInstaller @wrappers = options[:wrappers] @build_args = options[:build_args] @build_docs_in_background = options[:build_docs_in_background] + @install_as_default = options[:install_as_default] # Indicates that we should not try to update any deps unless # we absolutely must. @@ -93,13 +109,61 @@ class Gem::DependencyInstaller @cache_dir = options[:cache_dir] || @install_dir - # Set with any errors that SpecFetcher finds while search through - # gemspecs for a dep @errors = nil end - attr_reader :errors + ## + #-- + # TODO remove, no longer used + + def add_found_dependencies to_do, dependency_list # :nodoc: + seen = {} + dependencies = Hash.new { |h, name| h[name] = Gem::Dependency.new name } + + 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 + + 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 + + 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 = find_gems_with_sources(dep) + results.sorted.each do |t| + to_do.push t.spec + end + + results.remove_installed! dep + + @available << results + results.inject_into_list dependency_list + end + end + + dependency_list.remove_specs_unsatisfied_by dependencies + end ## # Creates an AvailableSet to install from based on +dep_or_name+ and # +version+ @@ -138,7 +202,7 @@ class Gem::DependencyInstaller # sources. Gems are sorted with newer gems preferred over older gems, and # local gems preferred over remote gems. - def find_gems_with_sources(dep) + def find_gems_with_sources dep # :nodoc: set = Gem::AvailableSet.new if consider_local? @@ -178,11 +242,51 @@ class Gem::DependencyInstaller set end + ## + # Finds a spec and the source_uri it came from for gem +gem_name+ and + # +version+. Returns an Array of specs and sources required for + # installation of the gem. + + def find_spec_by_name_and_version gem_name, + version = Gem::Requirement.default, + prerelease = false + set = Gem::AvailableSet.new + + if consider_local? + if gem_name =~ /\.gem$/ and File.file? gem_name then + src = Gem::Source::SpecificFile.new(gem_name) + set.add src.spec, src + else + local = Gem::Source::Local.new + + if s = local.find_gem(gem_name, version) + set.add s, local + end + end + end + + if set.empty? + dep = Gem::Dependency.new gem_name, version + dep.prerelease = true if prerelease + + set = find_gems_with_sources(dep) + set.match_platform! + end + + if set.empty? + raise Gem::SpecificGemNotFoundException.new(gem_name, version, @errors) + end + + @available = set + end + ## # Gathers all dependencies necessary for the installation from local and # remote sources unless the ignore_dependencies was given. + #-- + # TODO remove, no longer used - def gather_dependencies + def gather_dependencies # :nodoc: specs = @available.all_specs # these gems were listed by the user, always install them @@ -214,93 +318,19 @@ class Gem::DependencyInstaller @gems_to_install = dependency_list.dependency_order.reverse end - def add_found_dependencies to_do, dependency_list - seen = {} - dependencies = Hash.new { |h, name| h[name] = Gem::Dependency.new name } - - 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 - - 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 - - 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 = find_gems_with_sources(dep) - - results.sorted.each do |t| - to_do.push t.spec - end - - results.remove_installed! dep - - @available << results - results.inject_into_list dependency_list - end - end - - dependency_list.remove_specs_unsatisfied_by dependencies - end - - ## - # Finds a spec and the source_uri it came from for gem +gem_name+ and - # +version+. Returns an Array of specs and sources required for - # installation of the gem. - - def find_spec_by_name_and_version(gem_name, - version = Gem::Requirement.default, - prerelease = false) - - set = Gem::AvailableSet.new - - if consider_local? - if gem_name =~ /\.gem$/ and File.file? gem_name then - src = Gem::Source::SpecificFile.new(gem_name) - set.add src.spec, src - else - local = Gem::Source::Local.new - - if s = local.find_gem(gem_name, version) - set.add s, local + def in_background what # :nodoc: + 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 - - if set.empty? - dep = Gem::Dependency.new gem_name, version - # HACK Dependency objects should be immutable - dep.prerelease = true if prerelease - - set = find_gems_with_sources(dep) - set.match_platform! - end - - if set.empty? - raise Gem::SpecificGemNotFoundException.new(gem_name, version, @errors) - end - - @available = set + yield unless fork_happened end ## @@ -318,61 +348,30 @@ class Gem::DependencyInstaller # separately. def install dep_or_name, version = Gem::Requirement.default - available_set_for dep_or_name, version + request_set = resolve_dependencies dep_or_name, version @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 = @available.source_for spec - - begin - # 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 + options = { + :bin_dir => @bin_dir, + :build_args => @build_args, + :env_shebang => @env_shebang, + :force => @force, + :format_executable => @format_executable, + :ignore_dependencies => @ignore_dependencies, + :security_policy => @security_policy, + :user_install => @user_install, + :wrappers => @wrappers, + :install_as_default => @install_as_default + } + options[:install_dir] = @install_dir if @only_install_dir - inst = Gem::Installer.new local_gem_path, - :bin_dir => @bin_dir, - :development => is_dev, - :env_shebang => @env_shebang, - :force => @force, - :format_executable => @format_executable, - :ignore_dependencies => @ignore_dependencies, - :install_dir => @install_dir, - :security_policy => @security_policy, - :user_install => @user_install, - :wrappers => @wrappers, - :build_args => @build_args - - spec = inst.install - - @installed_gems << spec + request_set.install options do |_, installer| + @installed_gems << installer.spec if installer end + @installed_gems.sort! + # 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. @@ -385,18 +384,34 @@ class Gem::DependencyInstaller @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 + def install_development_deps # :nodoc: + if @development and @dev_shallow then + :shallow + elsif @development then + :all + else + :none end - yield unless fork_happened end + + def resolve_dependencies dep_or_name, version # :nodoc: + as = available_set_for dep_or_name, version + + request_set = as.to_request_set install_development_deps + request_set.soft_missing = @force + + installer_set = Gem::DependencyResolver::InstallerSet.new @domain + installer_set.always_install.concat request_set.always_install + installer_set.ignore_installed = @only_install_dir + + if @ignore_dependencies then + installer_set.ignore_dependencies = true + request_set.soft_missing = true + end + + request_set.resolve Gem::DependencyResolver.compose_sets(as, installer_set) + + request_set + end + end diff --git a/lib/rubygems/dependency_resolver.rb b/lib/rubygems/dependency_resolver.rb index 66f55eb9ad..abce692920 100644 --- a/lib/rubygems/dependency_resolver.rb +++ b/lib/rubygems/dependency_resolver.rb @@ -1,575 +1,254 @@ require 'rubygems' require 'rubygems/dependency' require 'rubygems/exceptions' +require 'rubygems/util/list' require 'uri' require 'net/http' -module Gem +## +# 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. - # 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 +class Gem::DependencyResolver - super "unable to resolve conflicting dependencies '#{a}' and '#{b}'" - end + ## + # Contains all the conflicts encountered while doing resolution - attr_reader :conflict + attr_reader :conflicts - def conflicting_dependencies - @conflict.conflicting_dependencies - end - end + attr_accessor :development - # 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}'" + attr_reader :missing - @dependency = dep - end + ## + # When a missing dependency, don't stop. Just go on and record what was + # missing. - attr_reader :dependency - end + attr_accessor :soft_missing - # 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 self.compose_sets *sets + Gem::DependencyResolver::ComposedSet.new(*sets) + end - def dependency - @request.dependency - end + ## + # Provide a DependencyResolver that queries only against the already + # installed gems. - attr_reader :conflicts + def self.for_current_gems needed + new needed, Gem::DependencyResolver::CurrentSet.new 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. + ## + # Create DependencyResolver object which will resolve the tree starting + # with +needed+ Depedency objects. # - 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 - attr_reader :set # :nodoc: - - 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 == other # :nodoc: - self.class === other and - @set == other.set and - @name == other.name and - @version == other.version and - @dependencies == other.dependencies - end - - 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] = [] } - @dep_uri = URI 'https://rubygems.org/api/v1/dependencies' - end - - # Return data for all versions of the gem +name+. - # - def versions(name) - if @data.key?(name) - return @data[name] - end - - uri = @dep_uri + "?gems=#{name}" - str = Gem::RemoteFetcher.fetcher.fetch_path uri - - 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? - - uri = @dep_uri + "?gems=#{needed.sort.join ','}" - str = Gem::RemoteFetcher.fetcher.fetch_path uri - - 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 + # +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 = nil + @set = set || Gem::DependencyResolver::IndexSet.new + @needed = needed + + @conflicts = nil + @development = false + @missing = [] + @soft_missing = false + 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 + def requests s, act, reqs=nil + s.dependencies.reverse_each do |d| + next if d.type == :development and not @development + reqs = Gem::List.new Gem::DependencyResolver::DependencyRequest.new(d, act), reqs end - # Provide a DependencyResolver that queries only against - # the already installed gems. - # - def self.for_current_gems(needed) - new needed, CurrentSet.new - end + @set.prefetch reqs - # Contains all the conflicts encountered while doing resolution - # - attr_reader :conflicts + reqs + end - # Proceed with resolution! Returns an array of ActivationRequest - # objects. - # - def resolve - @conflicts = [] + ## + # Proceed with resolution! Returns an array of ActivationRequest objects. - needed = @needed.map { |n| DependencyRequest.new(n, nil) } + def resolve + @conflicts = [] - res = resolve_for needed, [] + needed = nil - if res.kind_of? DependencyConflict - raise DependencyResolutionError.new(res) - end + @needed.reverse_each do |n| + request = Gem::DependencyResolver::DependencyRequest.new n, nil - res + needed = Gem::List.new request, needed 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 + res = resolve_for needed, nil - # Return the Specification that listed the dependency - # - def requester - @failed_dep.requester - end - - def for_spec?(spec) - @dependency.name == spec.name - end + raise Gem::DependencyResolutionError, res if + res.kind_of? Gem::DependencyResolver::DependencyConflict - # 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 + res.to_a + end - def ==(other) - case other - when Dependency - @dependency == other - when DependencyRequest - @dependency == other.dependency && @requester == other.requester + ## + # 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 + while needed + dep = needed.value + needed = needed.tail + + # If there is already a spec activated for the requested name... + if specs && 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 = + Gem::DependencyResolver::DependencyConflict.new dep, existing else - false + depreq = existing.request.requester.request + conflict = + Gem::DependencyResolver::DependencyConflict.new depreq, existing, dep 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 + @conflicts << conflict - def name - @spec.name + return conflict 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 - - Gem.ensure_gem_subdirectories path + # Get a list of all specs that satisfy dep and platform + possible = @set.find_all dep + possible = select_local_platforms possible - source.download full_spec, path - end + case possible.size + when 0 + @missing << dep - def ==(other) - case other - when Gem::Specification - @spec == other - when ActivationRequest - @spec == other.spec && @request == other.request - else - false + unless @soft_missing + # If there are none, then our work here is done. + raise Gem::UnsatisfiableDependencyError, dep 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 + 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 = Gem::DependencyResolver::ActivationRequest.new spec, dep, false + + specs = Gem::List.prepend 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 do |s| + [s.source, s.version, s.platform == Gem::Platform::RUBY ? -1 : 1] 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) + # 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 = Gem::DependencyResolver::ActivationRequest.new s, dep + + try = requests(s, act, needed) + + res = resolve_for try, Gem::List.prepend(specs, act) + + # While trying to resolve these dependencies, there may + # be a conflict! + + if res.kind_of? Gem::DependencyResolver::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 - depreq = existing.request.requester.request - conflict = DependencyConflict.new(depreq, existing, dep) + # No conflict, return the specs + return res end - @conflicts << conflict - - return conflict end - # Get a list of all specs that satisfy dep - possible = @set.find_all(dep) + # 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 Gem::ImpossibleDependenciesError.new(dep, conflicts) + end + end - 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 + specs + 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 + ## + # Returns the gems in +specs+ that match the local platform. - specs + def select_local_platforms specs # :nodoc: + specs.select do |spec| + Gem::Platform.match spec.platform end end + end + +require 'rubygems/dependency_resolver/api_set' +require 'rubygems/dependency_resolver/api_specification' +require 'rubygems/dependency_resolver/activation_request' +require 'rubygems/dependency_resolver/composed_set' +require 'rubygems/dependency_resolver/current_set' +require 'rubygems/dependency_resolver/dependency_conflict' +require 'rubygems/dependency_resolver/dependency_request' +require 'rubygems/dependency_resolver/index_set' +require 'rubygems/dependency_resolver/index_specification' +require 'rubygems/dependency_resolver/installed_specification' +require 'rubygems/dependency_resolver/installer_set' + diff --git a/lib/rubygems/exceptions.rb b/lib/rubygems/exceptions.rb index ff389b320b..4a988f9edf 100644 --- a/lib/rubygems/exceptions.rb +++ b/lib/rubygems/exceptions.rb @@ -7,7 +7,13 @@ # Base exception class for RubyGems. All exception raised by RubyGems are a # subclass of this one. class Gem::Exception < RuntimeError - attr_accessor :source_exception + + ## + #-- + # TODO: remove in RubyGems 3, nobody sets this + + attr_accessor :source_exception # :nodoc: + end class Gem::CommandLineError < Gem::Exception; end @@ -16,6 +22,28 @@ class Gem::DependencyError < Gem::Exception; end class Gem::DependencyRemovalException < Gem::Exception; end +## +# Raised by Gem::DependencyResolver when a Gem::DependencyConflict reaches the +# toplevel. Indicates which dependencies were incompatible through #conflict +# and #conflicting_dependencies + +class Gem::DependencyResolutionError < Gem::Exception + + attr_reader :conflict + + def initialize conflict + @conflict = conflict + a, b = conflicting_dependencies + + super "unable to resolve conflicting dependencies '#{a}' and '#{b}'" + end + + def conflicting_dependencies + @conflict.conflicting_dependencies + end + +end + ## # Raised when attempting to uninstall a gem that isn't in GEM_HOME. @@ -65,6 +93,42 @@ class Gem::SpecificGemNotFoundException < Gem::GemNotFoundException attr_reader :name, :version, :errors end +## +# Raised by Gem::DependencyResolver when dependencies conflict and create the +# inability to find a valid possible spec for a request. + +class Gem::ImpossibleDependenciesError < Gem::Exception + + attr_reader :conflicts + attr_reader :request + + def initialize request, conflicts + @request = request + @conflicts = conflicts + + super build_message + end + + def build_message # :nodoc: + requester = @request.requester + requester = requester ? requester.spec.full_name : 'The user' + dependency = @request.dependency + + message = "#{requester} requires #{dependency} but it conflicted:\n" + + @conflicts.each do |_, conflict| + message << conflict.explanation + end + + message + end + + def dependency + @request.dependency + end + +end + class Gem::InstallError < Gem::Exception; end ## @@ -107,3 +171,26 @@ class Gem::SystemExitException < SystemExit end +## +# Raised by DependencyResolver when a dependency requests a gem for which +# there is no spec. + +class Gem::UnsatisfiableDependencyError < Gem::Exception + + attr_reader :dependency + + def initialize dep + requester = dep.requester ? dep.requester.request : '(unknown)' + + super "Unable to resolve dependency: #{requester} requires #{dep}" + + @dependency = dep + end + +end + +## +# Backwards compatible typo'd exception class for early RubyGems 2.0.x + +Gem::UnsatisfiableDepedencyError = Gem::UnsatisfiableDependencyError # :nodoc: + diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb index 79cae906ee..8c05723573 100644 --- a/lib/rubygems/ext/builder.rb +++ b/lib/rubygems/ext/builder.rb @@ -4,8 +4,23 @@ # See LICENSE.txt for permissions. #++ +require 'rubygems/user_interaction' +require 'thread' + class Gem::Ext::Builder + include Gem::UserInteraction + + ## + # The builder shells-out to run various commands after changing the + # directory. This means multiple installations cannot be allowed to build + # extensions in parallel as they may change each other's directories leading + # to broken extensions or failed installations. + + CHDIR_MUTEX = Mutex.new # :nodoc: + + attr_accessor :build_args # :nodoc: + def self.class_name name =~ /Ext::(.*)Builder/ $1.downcase @@ -18,7 +33,7 @@ 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'] || ENV['make'] + make_program = ENV['MAKE'] || ENV['make'] || $1 unless make_program then make_program = (/mswin/ =~ RUBY_PLATFORM) ? 'nmake' : 'make' end @@ -63,5 +78,108 @@ class Gem::Ext::Builder end end + ## + # Creates a new extension builder for +spec+ using the given +build_args+. + # The gem for +spec+ is unpacked in +gem_dir+. + + def initialize spec, build_args + @spec = spec + @build_args = build_args + @gem_dir = spec.gem_dir + + @ran_rake = nil + end + + ## + # Chooses the extension builder class for +extension+ + + def builder_for extension # :nodoc: + case extension + when /extconf/ then + Gem::Ext::ExtConfBuilder + when /configure/ then + Gem::Ext::ConfigureBuilder + when /rakefile/i, /mkrf_conf/i then + @ran_rake = true + Gem::Ext::RakeBuilder + when /CMakeLists.txt/ then + Gem::Ext::CmakeBuilder + else + extension_dir = File.join @gem_dir, File.dirname(extension) + + message = "No builder for extension '#{extension}'" + build_error extension_dir, message + end + end + + ## + # Logs the build +output+ in +build_dir+, then raises ExtensionBuildError. + + def build_error build_dir, output, backtrace = nil # :nodoc: + gem_make_out = File.join build_dir, 'gem_make.out' + + open gem_make_out, 'wb' do |io| io.puts output end + + message = <<-EOF +ERROR: Failed to build gem native extension. + + #{output} + +Gem files will remain installed in #{@gem_dir} for inspection. +Results logged to #{gem_make_out} +EOF + + raise Gem::Installer::ExtensionBuildError, message, backtrace + end + + def build_extension extension, dest_path # :nodoc: + results = [] + + extension ||= '' # I wish I knew why this line existed + extension_dir = File.join @gem_dir, File.dirname(extension) + + builder = builder_for extension + + begin + FileUtils.mkdir_p dest_path + + CHDIR_MUTEX.synchronize do + Dir.chdir extension_dir do + results = builder.build(extension, @gem_dir, dest_path, + results, @build_args) + + say results.join("\n") if Gem.configuration.really_verbose + end + end + rescue + build_error extension_dir, results.join("\n"), $@ + end + end + + ## + # Builds extensions. Valid types of extensions are extconf.rb files, + # configure scripts and rakefiles or mkrf_conf files. + + def build_extensions + return if @spec.extensions.empty? + + 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 + + @spec.extensions.each do |extension| + break if @ran_rake + + build_extension extension, dest_path + end + end + end diff --git a/lib/rubygems/gem_runner.rb b/lib/rubygems/gem_runner.rb index 8060e15312..7a3fd6b116 100644 --- a/lib/rubygems/gem_runner.rb +++ b/lib/rubygems/gem_runner.rb @@ -33,17 +33,11 @@ class Gem::GemRunner ## # Run the gem command with the following arguments. - def run(args) - 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 + def run args + build_args = extract_build_args args do_configuration args + cmd = @command_manager_class.instance cmd.command_names.each do |command_name| @@ -60,6 +54,20 @@ class Gem::GemRunner cmd.run Gem.configuration.args, build_args end + ## + # Separates the build arguments (those following --) from the + # other arguments in the list. + + def extract_build_args args # :nodoc: + return [] unless offset = args.index('--') + + build_args = args.slice!(offset...args.length) + + build_args.shift + + build_args + end + private def do_configuration(args) diff --git a/lib/rubygems/gemcutter_utilities.rb b/lib/rubygems/gemcutter_utilities.rb index 04d7cd300f..9dbc18ba98 100644 --- a/lib/rubygems/gemcutter_utilities.rb +++ b/lib/rubygems/gemcutter_utilities.rb @@ -1,11 +1,17 @@ require 'rubygems/remote_fetcher' +## +# Utility methods for using the RubyGems API. + module Gem::GemcutterUtilities + # TODO: move to Gem::Command OptionParser.accept Symbol do |value| value.to_sym end + attr_writer :host + ## # Add the --key option @@ -17,6 +23,9 @@ module Gem::GemcutterUtilities end end + ## + # The API key from the command options or from the user's configuration. + def api_key if options[:key] then verify_api_key options[:key] @@ -27,35 +36,10 @@ module Gem::GemcutterUtilities end end - def sign_in sign_in_host = self.host - return if Gem.configuration.rubygems_api_key - - pretty_host = if Gem::DEFAULT_HOST == sign_in_host then - 'RubyGems.org' - else - sign_in_host - end - - say "Enter your #{pretty_host} credentials." - say "Don't have an account yet? " + - "Create one at #{sign_in_host}/sign_up" - - email = ask " Email: " - password = ask_for_password "Password: " - say "\n" - - response = rubygems_api_request(:get, "api/v1/api_key", - sign_in_host) do |request| - request.basic_auth email, password - end - - with_response response do |resp| - say "Signed in." - Gem.configuration.rubygems_api_key = resp.body - end - end + ## + # The host to connect to either from the RUBYGEMS_HOST environment variable + # or from the user's configuration - attr_writer :host def host configured_host = Gem.host unless Gem.configuration.disable_default_gem_server @@ -70,6 +54,9 @@ module Gem::GemcutterUtilities end end + ## + # Creates an RubyGems API to +host+ and +path+ with the given HTTP +method+. + def rubygems_api_request(method, path, host = nil, &block) require 'net/http' @@ -86,23 +73,43 @@ module Gem::GemcutterUtilities Gem::RemoteFetcher.fetcher.request(uri, request_method, &block) end - def with_response resp, error_prefix = nil - case resp - when Net::HTTPSuccess then - if block_given? then - yield resp - else - say resp.body - end - else - message = resp.body - message = "#{error_prefix}: #{message}" if error_prefix + ## + # Signs in with the RubyGems API at +sign_in_host+ and sets the rubygems API + # key. - say message - terminate_interaction 1 # TODO: question this + def sign_in sign_in_host = nil + sign_in_host ||= self.host + return if Gem.configuration.rubygems_api_key + + pretty_host = if Gem::DEFAULT_HOST == sign_in_host then + 'RubyGems.org' + else + sign_in_host + end + + say "Enter your #{pretty_host} credentials." + say "Don't have an account yet? " + + "Create one at #{sign_in_host}/sign_up" + + email = ask " Email: " + password = ask_for_password "Password: " + say "\n" + + response = rubygems_api_request(:get, "api/v1/api_key", + sign_in_host) do |request| + request.basic_auth email, password + end + + with_response response do |resp| + say "Signed in." + Gem.configuration.rubygems_api_key = resp.body end end + ## + # Retrieves the pre-configured API key +key+ or terminates interaction with + # an error. + def verify_api_key(key) if Gem.configuration.api_keys.key? key then Gem.configuration.api_keys[key] @@ -112,4 +119,29 @@ module Gem::GemcutterUtilities end end + ## + # If +response+ is an HTTP Success (2XX) response, yields the response if a + # block was given or shows the response body to the user. + # + # If the response was not successful, shows an error to the user including + # the +error_prefix+ and the response body. + + def with_response response, error_prefix = nil + case response + when Net::HTTPSuccess then + if block_given? then + yield response + else + say response.body + end + else + message = response.body + message = "#{error_prefix}: #{message}" if error_prefix + + say message + terminate_interaction 1 # TODO: question this + end + end + end + diff --git a/lib/rubygems/install_update_options.rb b/lib/rubygems/install_update_options.rb index ffa8f910df..d3f55cd5ea 100644 --- a/lib/rubygems/install_update_options.rb +++ b/lib/rubygems/install_update_options.rb @@ -26,6 +26,9 @@ module Gem::InstallUpdateOptions OptionParser.accept Gem::Security::Policy do |value| require 'rubygems/security' + raise OptionParser::InvalidArgument, 'OpenSSL not installed' unless + defined?(Gem::Security::HighSecurity) + value = Gem::Security::Policies[value] valid = Gem::Security::Policies.keys.sort message = "#{value} (#{valid.join ', '} are valid)" diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 0ccf7ad52f..261af890c8 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -9,7 +9,6 @@ require 'rubygems/package' require 'rubygems/ext' require 'rubygems/user_interaction' require 'fileutils' -require 'thread' ## # The installer installs the files contained in the .gem into the Gem.home. @@ -32,14 +31,6 @@ class Gem::Installer ENV_PATHS = %w[/usr/bin/env /bin/env] - ## - # The builder shells-out to run various commands after changing the - # directory. This means multiple installations cannot be allowed to build - # extensions in parallel as they may change each other's directories leading - # to broken extensions or failed installations. - - CHDIR_MUTEX = Mutex.new # :nodoc: - ## # Raised when there is an error while building extensions. # @@ -93,8 +84,8 @@ class Gem::Installer # :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. - # :format_executable:: Format the executable the same as the ruby executable. - # If your ruby is ruby18, foo_exec will be installed as + # :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. @@ -153,7 +144,7 @@ class Gem::Installer io.gets # blankline # TODO detect a specially formatted comment instead of trying - # to run a regexp against ruby code. + # to run a regexp against Ruby code. next unless io.gets =~ /This file was generated by RubyGems/ ruby_executable = true @@ -222,19 +213,24 @@ class Gem::Installer FileUtils.mkdir_p gem_dir - extract_files + spec.loaded_from = spec_file - build_extensions - write_build_info_file - run_post_build_hooks + if @options[:install_as_default] + extract_bin + write_default_spec + else + extract_files - generate_bin - write_spec - write_cache_file + build_extensions + write_build_info_file + run_post_build_hooks - say spec.post_install_message unless spec.post_install_message.nil? + generate_bin + write_spec + write_cache_file + end - spec.loaded_from = spec_file + say spec.post_install_message unless spec.post_install_message.nil? Gem::Specification.add_spec spec unless Gem::Specification.include? spec @@ -335,6 +331,14 @@ class Gem::Installer File.join gem_home, "specifications", "#{spec.full_name}.gemspec" end + ## + # The location of of the default spec file for default gems. + # + + def default_spec_file + File.join gem_home, "specifications/default", "#{spec.full_name}.gemspec" + end + ## # Writes the .gemspec specification (in Ruby) to the gem home's # specifications directory. @@ -346,6 +350,16 @@ class Gem::Installer end end + ## + # Writes the full .gemspec specification (in Ruby) to the gem home's + # specifications/default directory. + + def write_default_spec + File.open(default_spec_file, "w") do |file| + file.puts spec.to_ruby + end + end + ## # Creates windows .bat files for easy running of commands @@ -547,13 +561,13 @@ class Gem::Installer :bin_dir => nil, :env_shebang => false, :force => false, - :install_dir => Gem.dir, :only_install_dir => false }.merge options @env_shebang = options[:env_shebang] @force = options[:force] - @gem_home = options[:install_dir] + @install_dir = options[:install_dir] + @gem_home = options[:install_dir] || Gem.dir @ignore_dependencies = options[:ignore_dependencies] @format_executable = options[:format_executable] @security_policy = options[:security_policy] @@ -628,7 +642,7 @@ TEXT end ## - # return the stub script text used to launch the true ruby script + # return the stub script text used to launch the true Ruby script def windows_stub_script(bindir, bin_file_name) ruby = File.basename(Gem.ruby).chomp('"') @@ -647,75 +661,20 @@ TEXT # configure scripts and rakefiles or mkrf_conf files. def build_extensions - return if spec.extensions.empty? + builder = Gem::Ext::Builder.new spec, @build_args - 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 - - spec.extensions.each do |extension| - break if ran_rake - results = [] - - extension ||= "" - extension_dir = File.join gem_dir, File.dirname(extension) - - builder = case extension - when /extconf/ then - Gem::Ext::ExtConfBuilder - when /configure/ then - Gem::Ext::ConfigureBuilder - when /rakefile/i, /mkrf_conf/i then - ran_rake = true - Gem::Ext::RakeBuilder - when /CMakeLists.txt/ then - Gem::Ext::CmakeBuilder - else - message = "No builder for extension '#{extension}'" - extension_build_error extension_dir, message - end - - begin - FileUtils.mkdir_p dest_path - - CHDIR_MUTEX.synchronize do - Dir.chdir extension_dir do - results = builder.build(extension, gem_dir, dest_path, - results, @build_args) - - say results.join("\n") if Gem.configuration.really_verbose - end - end - rescue - extension_build_error(extension_dir, results.join("\n"), $@) - end - end + builder.build_extensions end ## # Logs the build +output+ in +build_dir+, then raises ExtensionBuildError. + # + # TODO: Delete this for RubyGems 3. It remains for API compatibility - def extension_build_error(build_dir, output, backtrace = nil) - gem_make_out = File.join build_dir, 'gem_make.out' - - open gem_make_out, 'wb' do |io| io.puts output end - - message = <<-EOF -ERROR: Failed to build gem native extension. - - #{output} - -Gem files will remain installed in #{gem_dir} for inspection. -Results logged to #{gem_make_out} -EOF + def extension_build_error(build_dir, output, backtrace = nil) # :nodoc: + builder = Gem::Ext::Builder.new spec, @build_args - raise ExtensionBuildError, message, backtrace + builder.build_error build_dir, output, backtrace end ## @@ -727,6 +686,15 @@ EOF @package.extract_files gem_dir end + ## + # Extracts only the bin/ files from the gem into the gem directory. + # This is used by default gems to allow a gem-aware stub to function + # without the full gem installed. + + def extract_bin + @package.extract_files gem_dir, "bin/*" + end + ## # Prefix and suffix the program filename the same as ruby. @@ -750,7 +718,7 @@ EOF ## # Performs various checks before installing the gem such as the install - # repository is writable and its directories exist, required ruby and + # repository is writable and its directories exist, required Ruby and # rubygems versions are met and that dependencies are installed. # # Version and dependency checks are skipped if this install is forced. @@ -767,7 +735,11 @@ EOF ensure_loadable_spec - Gem.ensure_gem_subdirectories gem_home + if options[:install_as_default] + Gem.ensure_default_gem_subdirectories gem_home + else + Gem.ensure_gem_subdirectories gem_home + end return true if @force diff --git a/lib/rubygems/name_tuple.rb b/lib/rubygems/name_tuple.rb index d16fad26f1..f16ab369fa 100644 --- a/lib/rubygems/name_tuple.rb +++ b/lib/rubygems/name_tuple.rb @@ -42,6 +42,20 @@ class Gem::NameTuple new nil, Gem::Version.new(0), nil end + ## + # Returns the full name (name-version) of this Gem. Platform information is + # included if it is not the default Ruby platform. This mimics the behavior + # of Gem::Specification#full_name. + + def full_name + case @platform + when nil, 'ruby', '' + "#{@name}-#{@version}" + else + "#{@name}-#{@version}-#{@platform}" + end + end + ## # Indicate if this NameTuple matches the current platform. @@ -59,12 +73,7 @@ class Gem::NameTuple # 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 + "#{full_name}.gemspec" end ## @@ -74,10 +83,12 @@ class Gem::NameTuple [@name, @version, @platform] end - def to_s + def inspect # :nodoc: "#" end + alias to_s inspect # :nodoc: + def <=> other to_a <=> other.to_a end diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index 957446257d..ba379c24cb 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.rb @@ -37,7 +37,7 @@ # 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 +# #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' @@ -280,11 +280,16 @@ EOM algorithms = if @checksums then @checksums.keys else - [Gem::Security::DIGEST_NAME] + [Gem::Security::DIGEST_NAME].compact end algorithms.each do |algorithm| - digester = OpenSSL::Digest.new algorithm + digester = + if defined?(OpenSSL::Digest) then + OpenSSL::Digest.new algorithm + else + Digest.const_get(algorithm).new + end digester << entry.read(16384) until entry.eof? @@ -298,8 +303,11 @@ EOM ## # Extracts the files in this package into +destination_dir+ + # + # If +pattern+ is specified, only entries matching that glob will be + # extracted. - def extract_files destination_dir + def extract_files destination_dir, pattern = "*" verify unless @spec FileUtils.mkdir_p destination_dir @@ -310,7 +318,7 @@ EOM reader.each do |entry| next unless entry.full_name == 'data.tar.gz' - extract_tar_gz entry, destination_dir + extract_tar_gz entry, destination_dir, pattern return # ignore further entries end @@ -324,11 +332,20 @@ EOM # If an entry in the archive contains a relative path above # +destination_dir+ or an absolute path is encountered an exception is # raised. + # + # If +pattern+ is specified, only entries matching that glob will be + # extracted. - def extract_tar_gz io, destination_dir # :nodoc: + def extract_tar_gz io, destination_dir, pattern = "*" # :nodoc: open_tar_gz io do |tar| tar.each do |entry| - destination = install_location entry.full_name, destination_dir + # Some entries start with "./" which fnmatch does not like, see github + # issue #644 + full_name = entry.full_name.sub %r%\A\./%, '' + + next unless File.fnmatch pattern, full_name + + destination = install_location full_name, destination_dir FileUtils.rm_rf destination @@ -428,12 +445,13 @@ EOM # certificate and key are not present only checksum generation is set up. def setup_signer + passphrase = ENV['GEM_PRIVATE_KEY_PASSPHRASE'] if @spec.signing_key then - @signer = Gem::Security::Signer.new @spec.signing_key, @spec.cert_chain + @signer = Gem::Security::Signer.new @spec.signing_key, @spec.cert_chain, passphrase @spec.signing_key = nil @spec.cert_chain = @signer.cert_chain.map { |cert| cert.to_s } else - @signer = Gem::Security::Signer.new nil, nil + @signer = Gem::Security::Signer.new nil, nil, passphrase @spec.cert_chain = @signer.cert_chain.map { |cert| cert.to_pem } if @signer.cert_chain end @@ -509,28 +527,39 @@ EOM end end + ## + # Verifies +entry+ in a .gem file. + + def verify_entry entry + file_name = entry.full_name + @files << file_name + + case file_name + when /\.sig$/ then + @signatures[$`] = entry.read if @security_policy + return + else + digest entry + end + + case file_name + when /^metadata(.gz)?$/ then + load_spec entry + when 'data.tar.gz' then + verify_gz entry + end + rescue => e + message = "package is corrupt, exception while verifying: " + + "#{e.message} (#{e.class})" + raise Gem::Package::FormatError.new message, @gem + 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 - else - digest entry - end - - case file_name - when /^metadata(.gz)?$/ then - load_spec entry - when 'data.tar.gz' then - verify_gz entry - end + verify_entry entry end unless @spec then diff --git a/lib/rubygems/package/tar_test_case.rb b/lib/rubygems/package/tar_test_case.rb index 4601f1328f..5253e32f36 100644 --- a/lib/rubygems/package/tar_test_case.rb +++ b/lib/rubygems/package/tar_test_case.rb @@ -71,7 +71,7 @@ class Gem::Package::TarTestCase < Gem::TestCase SP(Z(to_oct(sum, 6))) end - def header(type, fname, dname, length, mode, checksum = nil) + def header(type, fname, dname, length, mode, mtime, checksum = nil) checksum ||= " " * 8 arr = [ # struct tarfile_entry_posix @@ -80,7 +80,7 @@ class Gem::Package::TarTestCase < Gem::TestCase Z(to_oct(0, 7)), # char uid[8]; ditto Z(to_oct(0, 7)), # char gid[8]; ditto Z(to_oct(length, 11)), # char size[12]; 0 padded, octal, null - Z(to_oct(0, 11)), # char mtime[12]; 0 padded, octal, null + Z(to_oct(mtime, 11)), # char mtime[12]; 0 padded, octal, null checksum, # char checksum[8]; 0 padded, octal, null, space type, # char typeflag[1]; file: "0" dir: "5" "\0" * 100, # char linkname[100]; ASCII + (Z unless filled) @@ -105,16 +105,16 @@ class Gem::Package::TarTestCase < Gem::TestCase ret end - def tar_dir_header(name, prefix, mode) - h = header("5", name, prefix, 0, mode) + def tar_dir_header(name, prefix, mode, mtime) + h = header("5", name, prefix, 0, mode, mtime) checksum = calc_checksum(h) - header("5", name, prefix, 0, mode, checksum) + header("5", name, prefix, 0, mode, mtime, checksum) end - def tar_file_header(fname, dname, mode, length) - h = header("0", fname, dname, length, mode) + def tar_file_header(fname, dname, mode, length, mtime) + h = header("0", fname, dname, length, mode, mtime) checksum = calc_checksum(h) - header("0", fname, dname, length, mode, checksum) + header("0", fname, dname, length, mode, mtime, checksum) end def to_oct(n, pad_size) @@ -130,7 +130,7 @@ class Gem::Package::TarTestCase < Gem::TestCase end def util_dir_entry - util_entry tar_dir_header("foo", "bar", 0) + util_entry tar_dir_header("foo", "bar", 0, Time.now) end end diff --git a/lib/rubygems/package/tar_writer.rb b/lib/rubygems/package/tar_writer.rb index f2c11e3544..e1b38ad6b5 100644 --- a/lib/rubygems/package/tar_writer.rb +++ b/lib/rubygems/package/tar_writer.rb @@ -4,6 +4,8 @@ # See LICENSE.txt for additional licensing information. #++ +require 'digest' + ## # Allows writing of tar files @@ -121,7 +123,8 @@ class Gem::Package::TarWriter @io.pos = init_pos header = Gem::Package::TarHeader.new :name => name, :mode => mode, - :size => size, :prefix => prefix + :size => size, :prefix => prefix, + :mtime => Time.now @io.write header @io.pos = final_pos @@ -140,7 +143,15 @@ class Gem::Package::TarWriter def add_file_digest name, mode, digest_algorithms # :yields: io digests = digest_algorithms.map do |digest_algorithm| digest = digest_algorithm.new - [digest.name, digest] + digest_name = + if digest.respond_to? :name then + digest.name + else + /::([^:]+)$/ =~ digest_algorithm.name + $1 + end + + [digest_name, digest] end digests = Hash[*digests.flatten] @@ -165,22 +176,32 @@ class Gem::Package::TarWriter def add_file_signed name, mode, signer digest_algorithms = [ signer.digest_algorithm, - OpenSSL::Digest::SHA512, - ].uniq + Digest::SHA512, + ].compact.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 + signature_digest = digests.values.compact.find do |digest| + digest_name = + if digest.respond_to? :name then + digest.name + else + /::([^:]+)$/ =~ digest.class.name + $1 + end + + digest_name == signer.digest_name end - signature = signer.sign signature_digest.digest + if signer.key then + signature = signer.sign signature_digest.digest - add_file_simple "#{name}.sig", 0444, signature.length do |io| - io.write signature - end if signature + add_file_simple "#{name}.sig", 0444, signature.length do |io| + io.write signature + end + end digests end @@ -195,7 +216,8 @@ class Gem::Package::TarWriter name, prefix = split_name name header = Gem::Package::TarHeader.new(:name => name, :mode => mode, - :size => size, :prefix => prefix).to_s + :size => size, :prefix => prefix, + :mtime => Time.now).to_s @io.write header os = BoundedStream.new @io, size @@ -256,7 +278,8 @@ class Gem::Package::TarWriter header = Gem::Package::TarHeader.new :name => name, :mode => mode, :typeflag => "5", :size => 0, - :prefix => prefix + :prefix => prefix, + :mtime => Time.now @io.write header diff --git a/lib/rubygems/path_support.rb b/lib/rubygems/path_support.rb index 7195b322ef..2af303eecf 100644 --- a/lib/rubygems/path_support.rb +++ b/lib/rubygems/path_support.rb @@ -12,6 +12,10 @@ class Gem::PathSupport # Array of paths to search for Gems. attr_reader :path + ## + # Directory with spec cache + attr_reader :spec_cache_dir # :nodoc: + ## # # Constructor. Takes a single argument which is to be treated like a @@ -28,6 +32,12 @@ class Gem::PathSupport end self.path = env["GEM_PATH"] || ENV["GEM_PATH"] + + @spec_cache_dir = + env["GEM_SPEC_CACHE"] || ENV["GEM_SPEC_CACHE"] || + Gem.default_spec_cache_dir + + @spec_cache_dir = @spec_cache_dir.dup.untaint end private diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb index 4a4674b72f..247ee6ed3e 100644 --- a/lib/rubygems/platform.rb +++ b/lib/rubygems/platform.rb @@ -2,6 +2,8 @@ require "rubygems/deprecate" ## # Available list of platforms for targeting Gem installations. +# +# See `gem help platform` for information on platform matching. class Gem::Platform @@ -129,12 +131,16 @@ class Gem::Platform # Does +other+ match this platform? Two platforms match if they have the # same CPU, or either has a CPU of 'universal', they have the same OS, and # they have the same version, or either has no version. + # + # Additionally, the platform will match if the local CPU is 'arm' and the + # other CPU starts with "arm" (for generic ARM family support). def ===(other) return nil unless Gem::Platform === other # cpu - (@cpu == 'universal' or other.cpu == 'universal' or @cpu == other.cpu) and + (@cpu == 'universal' or other.cpu == 'universal' or @cpu == other.cpu or + (@cpu == 'arm' and other.cpu =~ /\Aarm/)) and # os @os == other.os and @@ -175,13 +181,13 @@ class Gem::Platform end ## - # A pure-ruby gem that may use Gem::Specification#extensions to build + # A pure-Ruby gem that may use Gem::Specification#extensions to build # binary files. RUBY = 'ruby' ## - # A platform-specific gem that is built for the packaging ruby's platform. + # A platform-specific gem that is built for the packaging Ruby's platform. # This will be replaced with Gem::Platform::local. CURRENT = 'current' diff --git a/lib/rubygems/psych_additions.rb b/lib/rubygems/psych_additions.rb index 08a5cb37ea..dcc13fdf2e 100644 --- a/lib/rubygems/psych_additions.rb +++ b/lib/rubygems/psych_additions.rb @@ -3,7 +3,7 @@ # in Specification._load, but if we don't have the constant, Marshal # blows up. -module Psych +module Psych # :nodoc: class PrivateType end end diff --git a/lib/rubygems/remote_fetcher.rb b/lib/rubygems/remote_fetcher.rb index 86bad9de41..6abd6bd9db 100644 --- a/lib/rubygems/remote_fetcher.rb +++ b/lib/rubygems/remote_fetcher.rb @@ -1,7 +1,7 @@ require 'rubygems' +require 'rubygems/request' +require 'rubygems/uri_formatter' require 'rubygems/user_interaction' -require 'thread' -require 'uri' require 'resolv' ## @@ -72,18 +72,7 @@ class Gem::RemoteFetcher Socket.do_not_reverse_lookup = true - @connections = {} - @connections_mutex = Mutex.new - @requests = Hash.new 0 - @proxy_uri = - case proxy - when :no_proxy then nil - when nil then get_proxy_from_env - when URI::HTTP then proxy - else URI.parse(proxy) - end - @user_agent = user_agent - @env_no_proxy = get_no_proxy_from_env + @proxy = proxy @dns = dns end @@ -202,7 +191,7 @@ class Gem::RemoteFetcher source_uri.path end - source_path = unescape source_path + source_path = Gem::UriFormatter.new(source_path).unescape begin FileUtils.cp source_path, local_gem_path unless @@ -321,128 +310,6 @@ class Gem::RemoteFetcher response['content-length'].to_i end - def escape(str) - return unless str - @uri_parser ||= uri_escaper - @uri_parser.escape str - end - - def unescape(str) - return unless str - @uri_parser ||= uri_escaper - @uri_parser.unescape str - end - - def uri_escaper - URI::Parser.new - rescue NameError - 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. - - def get_proxy_from_env - env_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY'] - - return nil if env_proxy.nil? or env_proxy.empty? - - uri = URI.parse(normalize_uri(env_proxy)) - - if uri and uri.user.nil? and uri.password.nil? then - # Probably we have http_proxy_* variables? - uri.user = escape(ENV['http_proxy_user'] || ENV['HTTP_PROXY_USER']) - uri.password = escape(ENV['http_proxy_pass'] || ENV['HTTP_PROXY_PASS']) - end - - uri - end - - ## - # Normalize the URI by adding "http://" if it is missing. - - def normalize_uri(uri) - (uri =~ /^(https?|ftp|file):/i) ? uri : "http://#{uri}" - end - - ## - # Creates or an HTTP connection based on +uri+, or retrieves an existing - # connection, using a proxy if needed. - - def connection_for(uri) - net_http_args = [uri.host, uri.port] - - if @proxy_uri and not no_proxy?(uri.host) then - net_http_args += [ - @proxy_uri.host, - @proxy_uri.port, - @proxy_uri.user, - @proxy_uri.password - ] - end - - connection_id = [Thread.current.object_id, *net_http_args].join ':' - - connection = @connections_mutex.synchronize do - @connections[connection_id] ||= Net::HTTP.new(*net_http_args) - @connections[connection_id] - end - - if https?(uri) and not connection.started? then - configure_connection_for_https(connection) - end - - connection.start unless connection.started? - - connection - rescue defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : Errno::EHOSTDOWN, - 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 - else - store.add_file Gem.configuration.ssl_ca_cert - end - else - store.set_default_paths - add_rubygems_trusted_certs(store) - end - connection.cert_store = store - rescue LoadError => e - raise unless (e.respond_to?(:path) && e.path == 'openssl') || - e.message =~ / -- openssl$/ - - raise Gem::Exception.new( - 'Unable to require openssl, install OpenSSL and rebuild ruby (preferred) or use non-HTTPS sources') - end - - def add_rubygems_trusted_certs(store) - 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 - def correct_for_windows_path(path) if path[0].chr == '/' && path[1].chr =~ /[a-z]/i && path[2].chr == ':' path = path[1..-1] @@ -451,136 +318,17 @@ class Gem::RemoteFetcher end end - 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 - ## # Performs a Net::HTTP request of type +request_class+ on +uri+ returning # a Net::HTTP response object. request maintains a table of persistent # connections to reduce connect overhead. def request(uri, request_class, last_modified = nil) - request = request_class.new uri.request_uri - - unless uri.nil? || uri.user.nil? || uri.user.empty? then - request.basic_auth uri.user, uri.password - end - - request.add_field 'User-Agent', @user_agent - request.add_field 'Connection', 'keep-alive' - request.add_field 'Keep-Alive', '30' - - if last_modified then - last_modified = last_modified.utc - request.add_field 'If-Modified-Since', last_modified.rfc2822 - end - - yield request if block_given? - - connection = connection_for uri - - retried = false - bad_response = false - - begin - @requests[connection.object_id] += 1 - - say "#{request.method} #{uri}" if - Gem.configuration.really_verbose - - file_name = File.basename(uri.path) - # perform download progress reporter only for gems - if request.response_body_permitted? && file_name =~ /\.gem$/ - reporter = ui.download_reporter - response = connection.request(request) do |incomplete_response| - if Net::HTTPOK === incomplete_response - reporter.fetch(file_name, incomplete_response.content_length) - downloaded = 0 - data = '' - - incomplete_response.read_body do |segment| - data << segment - downloaded += segment.length - reporter.update(downloaded) - end - reporter.done - if incomplete_response.respond_to? :body= - incomplete_response.body = data - else - incomplete_response.instance_variable_set(:@body, data) - end - end - end - else - response = connection.request request - end - - say "#{response.code} #{response.message}" if - Gem.configuration.really_verbose - - rescue Net::HTTPBadResponse - say "bad response" if Gem.configuration.really_verbose - - reset connection - - raise FetchError.new('too many bad responses', uri) if bad_response - - bad_response = true - retry - # HACK work around EOFError bug in Net::HTTP - # NOTE Errno::ECONNABORTED raised a lot on Windows, and make impossible - # to install gems. - rescue EOFError, Timeout::Error, - Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE - - requests = @requests[connection.object_id] - say "connection reset after #{requests} requests, retrying" if - Gem.configuration.really_verbose - - raise FetchError.new('too many connection resets', uri) if retried - - reset connection - - retried = true - retry - end + request = Gem::Request.new uri, request_class, last_modified, @proxy - response - end - - ## - # Resets HTTP connection +connection+. - - def reset(connection) - @requests.delete connection.object_id - - connection.finish - connection.start - end - - def user_agent - ua = "RubyGems/#{Gem::VERSION} #{Gem::Platform.local}" - - ruby_version = RUBY_VERSION - ruby_version += 'dev' if RUBY_PATCHLEVEL == -1 - - ua << " Ruby/#{ruby_version} (#{RUBY_RELEASE_DATE}" - if RUBY_PATCHLEVEL >= 0 then - ua << " patchlevel #{RUBY_PATCHLEVEL}" - elsif defined?(RUBY_REVISION) then - ua << " revision #{RUBY_REVISION}" + request.fetch do |req| + yield req if block_given? end - ua << ")" - - ua << " #{RUBY_ENGINE}" if defined?(RUBY_ENGINE) and RUBY_ENGINE != 'ruby' - - ua end def https?(uri) diff --git a/lib/rubygems/request_set.rb b/lib/rubygems/request_set.rb index 6c52b90c40..a45c64e0b4 100644 --- a/lib/rubygems/request_set.rb +++ b/lib/rubygems/request_set.rb @@ -5,178 +5,179 @@ require 'rubygems/dependency_list' require 'rubygems/installer' require 'tsort' -module Gem - class RequestSet +class Gem::RequestSet - include TSort + include TSort - def initialize(*deps) - @dependencies = deps + ## + # Array of gems to install even if already installed - yield self if block_given? - end + attr_reader :always_install - attr_reader :dependencies + 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 + attr_accessor :development - # Add +deps+ Gem::Depedency objects to the set. - # - def import(deps) - @dependencies += deps - end + ## + # Treat missing dependencies as silent errors - # 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 + attr_accessor :soft_missing - # 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 + def initialize *deps + @dependencies = deps - # Load a dependency management file. - # - def load_gemdeps(path) - gf = GemDepedencyAPI.new(self, path) - gf.load - end + @always_install = [] + @development = false + @requests = [] + @soft_missing = false + @sorted = nil + @specs = nil - def specs - @specs ||= @requests.map { |r| r.full_spec } - end + yield self if block_given? + end - def tsort_each_node(&block) - @requests.each(&block) - end + ## + # Declare that a gem of name +name+ with +reqs+ requirements is needed. - 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 gem name, *reqs + @dependencies << Gem::Dependency.new(name, reqs) + end + + ## + # Add +deps+ Gem::Dependency objects to the set. + + def import deps + @dependencies += deps + end - def sorted_requests - @sorted ||= strongly_connected_components.flatten + def install options, &block + if dir = options[:install_dir] + return install_into dir, false, options, &block end - def specs_in(dir) - Dir["#{dir}/specifications/*.gemspec"].map do |g| - Gem::Specification.load g + cache_dir = options[:cache_dir] || Gem.dir + + specs = [] + + sorted_requests.each do |req| + if req.installed? and + @always_install.none? { |spec| spec == req.spec.spec } then + yield req, nil if block_given? + next end - end - def install_into(dir, force=true, &b) - existing = force ? [] : specs_in(dir) + path = req.download cache_dir - dir = File.expand_path dir + inst = Gem::Installer.new path, options - installed = [] + yield req, inst if block_given? - sorted_requests.each do |req| - if existing.find { |s| s.full_name == req.spec.full_name } - b.call req, nil if b - next - end + specs << inst.install + end - path = req.download(dir) + specs + end - inst = Gem::Installer.new path, :install_dir => dir, - :only_install_dir => true + def install_into dir, force = true, options = {} + existing = force ? [] : specs_in(dir) + existing.delete_if { |s| @always_install.include? s } - b.call req, inst if b + dir = File.expand_path dir - inst.install + installed = [] - installed << req + sorted_requests.each do |req| + if existing.find { |s| s.full_name == req.spec.full_name } + yield req, nil if block_given? + next end - installed - end + path = req.download(dir) - def install(options, &b) - if dir = options[:install_dir] - return install_into(dir, false, &b) + unless path then # already installed + yield req, nil if block_given? + next end - cache_dir = options[:cache_dir] || Gem.dir + options[:install_dir] = dir + options[:only_install_dir] = true - specs = [] + inst = Gem::Installer.new path, options - sorted_requests.each do |req| - if req.installed? - b.call req, nil if b - next - end + yield req, inst if block_given? - path = req.download cache_dir + inst.install - inst = Gem::Installer.new path, options + installed << req + end - b.call req, inst if b + installed + end - specs << inst.install - end + ## + # Load a dependency management file. - specs - end + def load_gemdeps path + gf = Gem::RequestSet::GemDepedencyAPI.new self, path + gf.load + end - # A semi-compatible DSL for Bundler's Gemfile format - # - class GemDepedencyAPI - def initialize(set, path) - @set = set - @path = path - end + ## + # Resolve the requested dependencies and return an Array of Specification + # objects to be activated. - def load - instance_eval File.read(@path).untaint, @path, 1 - end + def resolve set = nil + resolver = Gem::DependencyResolver.new @dependencies, set + resolver.development = @development + resolver.soft_missing = @soft_missing - # DSL + @requests = resolver.resolve + end - def source(url) - end + ## + # Resolve the requested dependencies against the gems available via Gem.path + # and return an Array of Specification objects to be activated. - def gem(name, *reqs) - # Ignore the opts for now. - reqs.pop if reqs.last.kind_of?(Hash) + def resolve_current + resolve Gem::DependencyResolver::CurrentSet.new + end - @set.gem name, *reqs - end + def sorted_requests + @sorted ||= strongly_connected_components.flatten + end - def platform(what) - if what == :ruby - yield - end - end + def specs + @specs ||= @requests.map { |r| r.full_spec } + end + + def specs_in dir + Dir["#{dir}/specifications/*.gemspec"].map do |g| + Gem::Specification.load g + end + end + + def tsort_each_node &block # :nodoc: + @requests.each(&block) + end - alias_method :platforms, :platform + def tsort_each_child node # :nodoc: + node.spec.dependencies.each do |dep| + next if dep.type == :development and not @development - def group(*what) + match = @requests.find { |r| dep.match? r.spec.name, r.spec.version } + if match + begin + yield match + rescue TSort::Cyclic + end + else + unless @soft_missing + raise Gem::DependencyError, "Unresolved depedency found during sorting - #{dep}" + end end end end + end + +require 'rubygems/request_set/gem_dependency_api' diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb index bed47ab9f3..bfd6fd225b 100644 --- a/lib/rubygems/security.rb +++ b/lib/rubygems/security.rb @@ -12,20 +12,6 @@ begin rescue LoadError => e raise unless (e.respond_to?(:path) && e.path == 'openssl') || e.message =~ / -- openssl$/ - - module OpenSSL # :nodoc: - class Digest # :nodoc: - class SHA1 # :nodoc: - def name - 'SHA1' - end - end - end - module PKey # :nodoc: - class RSA # :nodoc: - end - end - end end ## @@ -352,23 +338,38 @@ module Gem::Security ## # Digest algorithm used to sign gems - DIGEST_ALGORITHM = OpenSSL::Digest::SHA1 + DIGEST_ALGORITHM = + if defined?(OpenSSL::Digest) then + OpenSSL::Digest::SHA1 + end ## # Used internally to select the signing digest from all computed digests - DIGEST_NAME = DIGEST_ALGORITHM.new.name # :nodoc: + DIGEST_NAME = # :nodoc: + if DIGEST_ALGORITHM then + DIGEST_ALGORITHM.new.name + end ## # Algorithm for creating the key pair used to sign gems - KEY_ALGORITHM = OpenSSL::PKey::RSA + KEY_ALGORITHM = + if defined?(OpenSSL::PKey) then + OpenSSL::PKey::RSA + end ## # Length of keys created by KEY_ALGORITHM KEY_LENGTH = 2048 + ## + # Cipher used to encrypt the key pair used to sign gems. + # Must be in the list returned by OpenSSL::Cipher.ciphers + + KEY_CIPHER = OpenSSL::Cipher.new('AES-256-CBC') if defined?(OpenSSL::Cipher) + ## # One year in seconds @@ -563,13 +564,18 @@ module Gem::Security ## # Writes +pemmable+, which must respond to +to_pem+ to +path+ with the given - # +permissions+. + # +permissions+. If passed +cipher+ and +passphrase+ those arguments will be + # passed to +to_pem+. - def self.write pemmable, path, permissions = 0600 + def self.write pemmable, path, permissions = 0600, passphrase = nil, cipher = KEY_CIPHER path = File.expand_path path open path, 'wb', permissions do |io| - io.write pemmable.to_pem + if passphrase and cipher + io.write pemmable.to_pem cipher, passphrase + else + io.write pemmable.to_pem + end end path @@ -579,8 +585,11 @@ module Gem::Security end -require 'rubygems/security/policy' -require 'rubygems/security/policies' +if defined?(OpenSSL::SSL) then + require 'rubygems/security/policy' + require 'rubygems/security/policies' + require 'rubygems/security/trust_dir' +end + require 'rubygems/security/signer' -require 'rubygems/security/trust_dir' diff --git a/lib/rubygems/security/policy.rb b/lib/rubygems/security/policy.rb index 467ee932b5..7238b2e477 100644 --- a/lib/rubygems/security/policy.rb +++ b/lib/rubygems/security/policy.rb @@ -1,3 +1,5 @@ +require 'rubygems/user_interaction' + ## # A Gem::Security::Policy object encapsulates the settings for verifying # signed gem files. This is the base class. You can either declare an @@ -6,6 +8,8 @@ class Gem::Security::Policy + include Gem::UserInteraction + attr_reader :name attr_accessor :only_signed @@ -175,6 +179,19 @@ class Gem::Security::Policy true end + ## + # Extracts the email or subject from +certificate+ + + def subject certificate # :nodoc: + certificate.extensions.each do |extension| + next unless extension.oid == 'subjectAltName' + + return extension.value + end + + certificate.subject.to_s + end + def inspect # :nodoc: ("[Policy: %s - data: %p signer: %p chain: %p root: %p " + "signed-only: %p trusted-only: %p]") % [ @@ -184,16 +201,24 @@ class Gem::Security::Policy end ## - # Verifies the certificate +chain+ is valid, the +digests+ match the - # signatures +signatures+ created by the signer depending on the +policy+ - # settings. + # For +full_name+, 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" + def verify chain, key = nil, digests = {}, signatures = {}, + full_name = '(unknown)' + if signatures.empty? then + if @only_signed then + raise Gem::Security::Exception, + "unsigned gems are not allowed by the #{name} policy" + elsif digests.empty? then + # lack of signatures is irrelevant if there is nothing to check + # against + else + alert_warning "#{full_name} is not signed" + end end opt = @opt @@ -222,7 +247,13 @@ class Gem::Security::Policy check_root chain, time if @verify_root - check_trust chain, digester, trust_dir if @only_trusted + if @only_trusted then + check_trust chain, digester, trust_dir + elsif signatures.empty? and digests.empty? then + # trust is irrelevant if there's no signatures to verify + else + alert_warning "#{subject signer} is not trusted for #{full_name}" + end signatures.each do |file, _| digest = signer_digests[file] @@ -252,7 +283,7 @@ class Gem::Security::Policy OpenSSL::X509::Certificate.new cert_pem end - verify chain, nil, digests, signatures + verify chain, nil, digests, signatures, spec.full_name true end diff --git a/lib/rubygems/security/signer.rb b/lib/rubygems/security/signer.rb index 78455c0732..bb1eae7cf2 100644 --- a/lib/rubygems/security/signer.rb +++ b/lib/rubygems/security/signer.rb @@ -29,7 +29,7 @@ class Gem::Security::Signer # +chain+ containing X509 certificates, encoding certificates or paths to # certificates. - def initialize key, cert_chain + def initialize key, cert_chain, passphrase = nil @cert_chain = cert_chain @key = key @@ -46,7 +46,7 @@ class Gem::Security::Signer @digest_algorithm = Gem::Security::DIGEST_ALGORITHM @digest_name = Gem::Security::DIGEST_NAME - @key = OpenSSL::PKey::RSA.new File.read @key if + @key = OpenSSL::PKey::RSA.new File.read(@key), passphrase if @key and not OpenSSL::PKey::RSA === @key if @cert_chain then @@ -62,6 +62,22 @@ class Gem::Security::Signer end end + ## + # Extracts the full name of +cert+. If the certificate has a subjectAltName + # this value is preferred, otherwise the subject is used. + + def extract_name cert # :nodoc: + subject_alt_name = cert.extensions.find { |e| 'subjectAltName' == e.oid } + + if subject_alt_name then + /\Aemail:/ =~ subject_alt_name.value + + $' || subject_alt_name.value + else + cert.subject + end + end + ## # Loads any missing issuers in the cert chain from the trusted certificates. # @@ -89,7 +105,9 @@ class Gem::Security::Signer re_sign_key end - Gem::Security::SigningPolicy.verify @cert_chain, @key + full_name = extract_name @cert_chain.last + + Gem::Security::SigningPolicy.verify @cert_chain, @key, {}, {}, full_name @key.sign @digest_algorithm.new, data end diff --git a/lib/rubygems/server.rb b/lib/rubygems/server.rb index dd582193ee..3ca588ae92 100644 --- a/lib/rubygems/server.rb +++ b/lib/rubygems/server.rb @@ -670,13 +670,13 @@ div.method-source-code pre { color: #ffdead; overflow: hidden; } # documentation for the particular gem, otherwise a list with results is # shown. # - # === Additional trick - install documentation for ruby core + # === Additional trick - install documentation for Ruby core # # Note: please adjust paths accordingly use for example 'locate yaml.rb' and # 'gem environment' to identify directories, that are specific for your # local installation # - # 1. install ruby sources + # 1. install Ruby sources # cd /usr/src # sudo apt-get source ruby # @@ -702,7 +702,7 @@ div.method-source-code pre { color: #ffdead; overflow: hidden; } # name pattern was found. # # The search is based on the file system content, not on the gems metadata. - # This allows additional documentation folders like 'core' for the ruby core + # This allows additional documentation folders like 'core' for the Ruby core # documentation - just put it underneath the main doc folder. def show_rdoc_for_pattern(pattern, res) diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb index 96d57870e2..f0e2a597b9 100644 --- a/lib/rubygems/source.rb +++ b/lib/rubygems/source.rb @@ -25,14 +25,23 @@ class Gem::Source end def <=>(other) - if !@uri - return 0 unless other.uri - return -1 - end + case other + when Gem::Source::Installed, + Gem::Source::Local, + Gem::Source::SpecificFile then + -1 + when Gem::Source then + if !@uri + return 0 unless other.uri + return 1 + end - return 1 if !other.uri + return -1 if !other.uri - @uri.to_s <=> other.uri.to_s + @uri.to_s <=> other.uri.to_s + else + nil + end end include Comparable @@ -58,8 +67,7 @@ class Gem::Source 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) + File.join Gem.spec_cache_dir, "#{uri.host}%#{uri.port}", File.dirname(escaped_path) end def update_cache? @@ -141,4 +149,16 @@ class Gem::Source fetcher = Gem::RemoteFetcher.fetcher fetcher.download spec, @uri.to_s, dir end + + def pretty_print q # :nodoc: + q.group 2, '[Remote:', ']' do + q.breakable + q.text @uri.to_s + end + end + end + +require 'rubygems/source/installed' +require 'rubygems/source/specific_file' +require 'rubygems/source/local' diff --git a/lib/rubygems/source_local.rb b/lib/rubygems/source_local.rb index 44b170c4a4..0808f4694a 100644 --- a/lib/rubygems/source_local.rb +++ b/lib/rubygems/source_local.rb @@ -1,92 +1,5 @@ require 'rubygems/source' +require 'rubygems/source_local' -class Gem::Source::Local < Gem::Source - def initialize - @uri = nil - end +# TODO warn upon require, this file is deprecated. - 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 index d296e617cc..f785c2667c 100644 --- a/lib/rubygems/source_specific_file.rb +++ b/lib/rubygems/source_specific_file.rb @@ -1,28 +1,4 @@ -class Gem::Source::SpecificFile < Gem::Source - def initialize(file) - @uri = nil - @path = ::File.expand_path(file) +require 'rubygems/source/specific_file' - @package = Gem::Package.new @path - @spec = @package.spec - @name = @spec.name_tuple - end +# TODO warn upon require, this file is deprecated. - 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 3d484d1c13..2ed7d4286a 100644 --- a/lib/rubygems/spec_fetcher.rb +++ b/lib/rubygems/spec_fetcher.rb @@ -38,7 +38,6 @@ class Gem::SpecFetcher end def initialize - @dir = File.join Gem.user_home, '.gem', 'specs' @update_cache = File.stat(Gem.user_home).uid == Process.uid @specs = {} @@ -75,6 +74,12 @@ class Gem::SpecFetcher list, errors = available_specs(type) list.each do |source, specs| + if dependency.name.is_a?(String) && specs.respond_to?(:bsearch) + start_index = (0 ... specs.length).bsearch{ |i| specs[i].name >= dependency.name } + end_index = (0 ... specs.length).bsearch{ |i| specs[i].name > dependency.name } + specs = specs[start_index ... end_index] if start_index && end_index + end + found[source] = specs.select do |tup| if dependency.match?(tup) if matching_platform and !Gem::Platform.match(tup.platform) @@ -195,8 +200,11 @@ class Gem::SpecFetcher when :released tuples_for source, :released when :complete - tuples_for(source, :prerelease, true) + + names = + tuples_for(source, :prerelease, true) + tuples_for(source, :released) + + names.sort when :prerelease tuples_for(source, :prerelease) else @@ -215,15 +223,15 @@ class Gem::SpecFetcher def tuples_for(source, type, gracefully_ignore=false) cache = @caches[type] - if gracefully_ignore + tuples = begin cache[source.uri] ||= source.load_specs(type) rescue Gem::RemoteFetcher::FetchError + raise unless gracefully_ignore [] end - else - cache[source.uri] ||= source.load_specs(type) - end + + tuples.sort_by { |tup| tup.name } end end diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb index 87557cee5f..12943a3e24 100644 --- a/lib/rubygems/specification.rb +++ b/lib/rubygems/specification.rb @@ -5,10 +5,13 @@ # See LICENSE.txt for permissions. #++ + require 'rubygems/version' require 'rubygems/requirement' require 'rubygems/platform' require 'rubygems/deprecate' +require 'rubygems/basic_specification' +require 'rubygems/stub_specification' # :stopdoc: # date.rb can't be loaded for `make install` due to miniruby @@ -31,7 +34,7 @@ class Date; end # s.homepage = 'https://rubygems.org/gems/example' # end # -# Starting in RubyGems 1.9.0, a Specification can hold arbitrary +# Starting in RubyGems 2.0, a Specification can hold arbitrary # metadata. This metadata is accessed via Specification#metadata # and has the following restrictions: # @@ -45,7 +48,7 @@ class Date; end # # s.metadata = { "bugtracker" => "http://somewhere.com/blah" } -class Gem::Specification +class Gem::Specification < Gem::BasicSpecification # REFACTOR: Consider breaking out this version stuff into a separate # module. There's enough special stuff around it that it may justify @@ -107,6 +110,10 @@ class Gem::Specification today = Time.now.utc TODAY = Time.utc(today.year, today.month, today.day) + LOAD_CACHE = {} + + private_constant :LOAD_CACHE if defined? private_constant + # :startdoc: ## @@ -156,6 +163,17 @@ class Gem::Specification :version => nil, } + Dupable = { } + + @@default_value.each do |k,v| + case v + when Time, Numeric, Symbol, true, false, nil + Dupable[k] = false + else + Dupable[k] = true + end + end + @@attributes = @@default_value.keys.sort_by { |s| s.to_s } @@array_attributes = @@default_value.reject { |k,v| v != [] }.keys @@nil_attributes, @@non_nil_attributes = @@default_value.keys.partition { |k| @@ -440,7 +458,7 @@ class Gem::Specification # # 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 + # found in bindir. These files must be executable Ruby files. Files that # use bash or other interpreters will not work. # # Usage: @@ -598,18 +616,13 @@ class Gem::Specification attr_writer :default_executable - ## - # Path this gemspec was loaded from. This attribute is not persisted. - - attr_reader :loaded_from - ## # Allows deinstallation of gems with legacy platforms. attr_writer :original_platform # :nodoc: ## - # The version of ruby required by this gem + # The version of Ruby required by this gem attr_reader :required_ruby_version @@ -633,58 +646,68 @@ class Gem::Specification attr_accessor :specification_version - class << self - def default_specifications_dir - File.join(Gem.default_dir, "specifications", "default") + def self._all # :nodoc: + unless defined?(@@all) && @@all then + @@all = stubs.map(&:to_spec) + + # After a reset, make sure already loaded specs + # are still marked as activated. + specs = {} + Gem.loaded_specs.each_value{|s| specs[s] = true} + @@all.each{|s| s.activated = true if specs[s]} + + _resort!(@@all) end + @@all + end - 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 - yield(spec) if spec - } - } + def self._clear_load_cache # :nodoc: + LOAD_CACHE.clear + end + + # :nodoc: + def self.each_gemspec(dirs) + dirs.each do |dir| + Dir[File.join(dir, "*.gemspec")].each do |path| + yield path.untaint + end end + end - def each_default(&block) # :nodoc: - each_spec([default_specifications_dir], - &block) + # :nodoc: + def self.each_stub(dirs) + each_gemspec(dirs) do |path| + stub = Gem::StubSpecification.new(path) + yield stub if stub.valid? end + end - def each_normal(&block) # :nodoc: - each_spec(dirs, &block) + # :nodoc: + def self.each_spec(dirs) + each_gemspec(dirs) do |path| + spec = self.load path + yield spec if spec end end - def self._all # :nodoc: - unless defined?(@@all) && @@all then + ## + # Returns a Gem::StubSpecification for every installed gem - specs = {} - each_default do |spec| - specs[spec.full_name] ||= spec + def self.stubs + @@stubs ||= begin + stubs = {} + each_stub([default_specifications_dir] + dirs) do |stub| + stubs[stub.full_name] ||= stub end - each_normal do |spec| - specs[spec.full_name] ||= spec - end - - @@all = specs.values - # After a reset, make sure already loaded specs - # are still marked as activated. - specs = {} - Gem.loaded_specs.each_value{|s| specs[s] = true} - @@all.each{|s| s.activated = true if specs[s]} - - _resort! + stubs = stubs.values + _resort!(stubs) + stubs end - @@all end - def self._resort! # :nodoc: - @@all.sort! { |a, b| + def self._resort!(specs) # :nodoc: + specs.sort! { |a, b| names = a.name <=> b.name next names if names.nonzero? b.version <=> a.version @@ -695,7 +718,9 @@ class Gem::Specification # Loads the default specifications. It should be called only once. def self.load_defaults - each_default do |spec| + each_spec([default_specifications_dir]) do |spec| + # #load returns nil if the spec is bad, so we just ignore + # it at this stage Gem.register_default_spec(spec) end end @@ -718,7 +743,9 @@ class Gem::Specification return if _all.include? spec _all << spec - _resort! + stubs << spec + _resort!(_all) + _resort!(stubs) end ## @@ -759,7 +786,7 @@ class Gem::Specification # -- wilsonb def self.all= specs - @@all = specs + @@all = @@stubs = specs end ## @@ -861,9 +888,10 @@ class Gem::Specification # 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? + stub = stubs.find { |s| + s.contains_requirable_file? path unless s.activated? } + stub && stub.to_spec end ## @@ -944,7 +972,7 @@ class Gem::Specification result.map(&:last).map(&:values).flatten.reject { |spec| minimum = native[spec.name] minimum && spec.version < minimum - } + }.sort_by{ |tup| tup.name } end ## @@ -955,6 +983,9 @@ class Gem::Specification file = file.dup.untaint return unless File.file?(file) + spec = LOAD_CACHE[file] + return spec if spec + code = if defined? Encoding File.read file, :mode => 'r:UTF-8:-' else @@ -968,6 +999,7 @@ class Gem::Specification if Gem::Specification === spec spec.loaded_from = file.to_s + LOAD_CACHE[file] = spec return spec end @@ -1002,25 +1034,43 @@ class Gem::Specification end ## - # Return a list of all outdated specifications. This method is HEAVY + # Return a list of all outdated local gem names. This method is HEAVY # as it must go fetch specifications from the server. + # + # Use outdated_and_latest_version if you wish to retrieve the latest remote + # version as well. def self.outdated - outdateds = [] + outdated_and_latest_version.map { |local, _| local.name } + end + + ## + # Enumerates the outdated local gems yielding the local specification and + # the latest remote version. + # + # This method may take some time to return as it must check each local gem + # against the server's index. + + def self.outdated_and_latest_version + return enum_for __method__ unless block_given? # TODO: maybe we should switch to rubygems' version service? fetcher = Gem::SpecFetcher.fetcher - latest_specs(true).each do |local| - dependency = Gem::Dependency.new local.name, ">= #{local.version}" - remotes, _ = fetcher.search_for_dependency dependency - remotes = remotes.map { |n, _| n.version } - latest = remotes.sort.last + latest_specs(true).each do |local_spec| + dependency = + Gem::Dependency.new local_spec.name, ">= #{local_spec.version}" - outdateds << local.name if latest and local.version < latest + remotes, = fetcher.search_for_dependency dependency + remotes = remotes.map { |n, _| n.version } + + latest_remote = remotes.sort.last + + yield [local_spec, latest_remote] if + latest_remote and local_spec.version < latest_remote end - outdateds + nil end ## @@ -1031,6 +1081,7 @@ class Gem::Specification raise "wtf: #{spec.full_name} not in #{all_names.inspect}" unless _all.include? spec _all.delete spec + stubs.delete_if { |s| s.full_name == spec.full_name } end ## @@ -1055,6 +1106,8 @@ class Gem::Specification @@dirs = nil Gem.pre_reset_hooks.each { |hook| hook.call } @@all = nil + @@stubs = nil + _clear_load_cache unresolved = unresolved_deps unless unresolved.empty? then w = "W" + "ARN" @@ -1300,20 +1353,6 @@ class Gem::Specification @authors ||= [] end - ## - # Returns the full path to the base gem directory. - # - # eg: /usr/local/lib/ruby/gems/1.8 - - def base_dir - return Gem.dir unless loaded_from - @base_dir ||= if default_gem? then - File.dirname File.dirname File.dirname loaded_from - else - File.dirname File.dirname loaded_from - end - end - ## # Returns the full path to installed gem's bin directory. # @@ -1387,19 +1426,6 @@ class Gem::Specification conflicts end - ## - # Return true if this spec can require +file+. - - def contains_requirable_file? file - root = full_gem_path - suffixes = Gem.suffixes - - require_paths.any? do |lib| - base = "#{root}/#{lib}/#{file}" - suffixes.any? { |suf| File.file? "#{base}#{suf}" } - end - end - ## # The date this gem was created. Lazily defaults to the current UTC date. # @@ -1645,35 +1671,14 @@ class Gem::Specification spec end - ## - # The full path to the gem (install path + full name). - - def full_gem_path - # 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 - - @full_gem_path = File.expand_path File.join(gems_dir, original_name) + # :nodoc: + def find_full_gem_path + super || File.expand_path(File.join(gems_dir, original_name)) end - - ## - # Returns the full name (name-version) of this Gem. Platform information - # is included (name-version-platform) if it is specified and not the - # default Ruby platform. + private :find_full_gem_path def full_name - @full_name ||= if platform == Gem::Platform::RUBY or platform.nil? then - "#{@name}-#{@version}".untaint - else - "#{@name}-#{@version}-#{platform}".untaint - end + @full_name ||= super end ## @@ -1684,15 +1689,6 @@ class Gem::Specification @gem_dir ||= File.expand_path File.join(gems_dir, full_name) end - ## - # Returns the full path to the gems directory containing this spec's - # gem directory. eg: /usr/local/lib/ruby/1.8/gems - - def gems_dir - # TODO: this logic seems terribly broken, but tests fail if just base_dir - @gems_dir ||= File.join(loaded_from && base_dir || Gem.dir, "gems") - end - ## # Deprecated and ignored, defaults to true. # @@ -1725,9 +1721,7 @@ class Gem::Specification # :startdoc: def hash # :nodoc: - @@attributes.inject(0) { |hash_code, (name, _)| - hash_code ^ self.send(name).hash - } + name.hash ^ version.hash end def init_with coder # :nodoc: @@ -1742,7 +1736,7 @@ class Gem::Specification def initialize name = nil, version = nil @loaded = false @activated = false - @loaded_from = nil + self.loaded_from = nil @original_platform = nil @@nil_attributes.each do |key| @@ -1751,11 +1745,7 @@ class Gem::Specification @@non_nil_attributes.each do |key| default = default_value(key) - value = case default - when Time, Numeric, Symbol, true, false, nil then default - else default.dup - end - + value = Dupable[key] ? default.dup : default instance_variable_set "@#{key}", value end @@ -1850,22 +1840,14 @@ class Gem::Specification @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 + def loaded_from= path # :nodoc: + super - # 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 @@ -2050,6 +2032,10 @@ class Gem::Specification @requirements = Array req end + def respond_to_missing? m, include_private = false # :nodoc: + false + end + ## # Returns the full path to this spec's ri directory. @@ -2111,10 +2097,16 @@ class Gem::Specification # Returns an object you can use to sort specifications in #sort_by. def sort_obj - # TODO: this is horrible. Deprecate it. [@name, @version, @new_platform == Gem::Platform::RUBY ? -1 : 1] end + ## + # Used by Gem::DependencyResolver to order Gem::Specification objects + + def source # :nodoc: + self + end + ## # Returns the full path to the directory containing this spec's # gemspec file. eg: /usr/local/lib/ruby/gems/1.8/specifications @@ -2194,6 +2186,7 @@ class Gem::Specification mark_version result = [] result << "# -*- encoding: utf-8 -*-" + result << "#{Gem::StubSpecification::PREFIX}#{name} #{version} #{platform} #{require_paths.join("\0")}" result << nil result << "Gem::Specification.new do |s|" @@ -2281,6 +2274,13 @@ class Gem::Specification "#" end + ## + # Returns self + + def to_spec + self + end + def to_yaml(opts = {}) # :nodoc: if YAML.const_defined?(:ENGINE) && !YAML::ENGINE.syck? then # Because the user can switch the YAML engine behind our @@ -2581,11 +2581,6 @@ class Gem::Specification end end - def default_gem? - loaded_from && - File.dirname(loaded_from) == self.class.default_specifications_dir - end - extend Gem::Deprecate # TODO: diff --git a/lib/rubygems/test_case.rb b/lib/rubygems/test_case.rb index 7d087afade..d6c1a36ad1 100644 --- a/lib/rubygems/test_case.rb +++ b/lib/rubygems/test_case.rb @@ -58,7 +58,7 @@ module Gem end ## - # Allows setting path to ruby. This method is available when requiring + # Allows setting path to Ruby. This method is available when requiring # 'rubygems/test_case' def self.ruby= ruby @@ -84,6 +84,23 @@ end class Gem::TestCase < MiniTest::Unit::TestCase + def assert_activate expected, *specs + specs.each do |spec| + case spec + when String then + Gem::Specification.find_by_name(spec).activate + when Gem::Specification then + spec.activate + else + flunk spec.inspect + end + end + + loaded = Gem.loaded_specs.values.map(&:full_name) + + assert_equal expected.sort, loaded.sort if expected + end + # TODO: move to minitest def assert_path_exists path, msg = nil msg = message(msg) { "Expected path '#{path}' to exist" } @@ -206,10 +223,11 @@ class Gem::TestCase < MiniTest::Unit::TestCase @gemhome = File.join @tempdir, 'gemhome' @userhome = File.join @tempdir, 'userhome' + ENV["GEM_SPEC_CACHE"] = File.join @tempdir, 'spec_cache' @orig_ruby = if ENV['RUBY'] then - ruby = Gem.instance_variable_get :@ruby - Gem.instance_variable_set :@ruby, ENV['RUBY'] + ruby = Gem.ruby + Gem.ruby = ENV['RUBY'] ruby end @@ -227,6 +245,9 @@ class Gem::TestCase < MiniTest::Unit::TestCase FileUtils.mkdir_p @gemhome FileUtils.mkdir_p @userhome + @orig_gem_private_key_passphrase = ENV['GEM_PRIVATE_KEY_PASSPHRASE'] + ENV['GEM_PRIVATE_KEY_PASSPHRASE'] = PRIVATE_KEY_PASSPHRASE + @default_dir = File.join @tempdir, 'default' @default_spec_dir = File.join @default_dir, "specifications", "default" Gem.instance_variable_set :@default_dir, @default_dir @@ -272,39 +293,6 @@ class Gem::TestCase < MiniTest::Unit::TestCase end @marshal_version = "#{Marshal::MAJOR_VERSION}.#{Marshal::MINOR_VERSION}" - - # 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 - Gem.post_build do |installer| - @post_build_hook_arg = installer - true - end - - Gem.post_install do |installer| - @post_install_hook_arg = installer - end - - Gem.post_uninstall do |uninstaller| - @post_uninstall_hook_arg = uninstaller - end - - Gem.pre_install do |installer| - @pre_install_hook_arg = installer - true - end - - Gem.pre_uninstall do |uninstaller| - @pre_uninstall_hook_arg = uninstaller - end end ## @@ -328,8 +316,7 @@ class Gem::TestCase < MiniTest::Unit::TestCase ENV['GEM_HOME'] = @orig_gem_home ENV['GEM_PATH'] = @orig_gem_path - _ = @orig_ruby - Gem.instance_variable_set :@ruby, @orig_ruby if @orig_ruby + Gem.ruby = @orig_ruby if @orig_ruby if @orig_ENV_HOME then ENV['HOME'] = @orig_ENV_HOME @@ -338,6 +325,47 @@ class Gem::TestCase < MiniTest::Unit::TestCase end Gem.instance_variable_set :@default_dir, nil + + ENV['GEM_PRIVATE_KEY_PASSPHRASE'] = @orig_gem_private_key_passphrase + + Gem::Specification._clear_load_cache + end + + def common_installer_setup + common_installer_teardown + + Gem.post_build do |installer| + @post_build_hook_arg = installer + true + end + + Gem.post_install do |installer| + @post_install_hook_arg = installer + end + + Gem.post_uninstall do |uninstaller| + @post_uninstall_hook_arg = uninstaller + end + + Gem.pre_install do |installer| + @pre_install_hook_arg = installer + true + end + + Gem.pre_uninstall do |uninstaller| + @pre_uninstall_hook_arg = uninstaller + end + end + + def common_installer_teardown + 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 end ## @@ -566,6 +594,21 @@ class Gem::TestCase < MiniTest::Unit::TestCase end end + def loaded_spec_names + Gem.loaded_specs.values.map(&:full_name).sort + end + + def unresolved_names + Gem::Specification.unresolved_deps.values.map(&:to_s).sort + end + + def save_loaded_features + old_loaded_features = $LOADED_FEATURES.dup + yield + ensure + $LOADED_FEATURES.replace old_loaded_features + 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. @@ -742,7 +785,7 @@ 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) + @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] @@ -762,14 +805,15 @@ Also, a list: util_build_gem @a2_pre end - write_file File.join(*%W[gems #{@a1.original_name} lib code.rb]) - write_file File.join(*%W[gems #{@a2.original_name} lib code.rb]) - write_file File.join(*%W[gems #{@a3a.original_name} lib code.rb]) - 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]) + write_file File.join(*%W[gems #{@a1.original_name} lib code.rb]) + write_file File.join(*%W[gems #{@a2.original_name} lib code.rb]) + write_file File.join(*%W[gems #{@a3a.original_name} lib code.rb]) + write_file File.join(*%W[gems #{@a_evil9.original_name} lib code.rb]) + 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, @x, @dep_x].each do |spec| util_build_gem spec @@ -972,7 +1016,7 @@ Also, a list: end ## - # Finds the path to the ruby executable + # Finds the path to the Ruby executable def self.rubybin ruby = ENV["RUBY"] @@ -1011,6 +1055,24 @@ Also, a list: Gem::Dependency.new name, *requirements end + ## + # Constructs a Gem::DependencyResolver::DependencyRequest from a + # Gem::Dependency +dep+, a +from_name+ and +from_version+ requesting the + # dependency and a +parent+ DependencyRequest + + def dependency_request dep, from_name, from_version, parent = nil + remote = Gem::Source.new @uri + + parent ||= Gem::DependencyResolver::DependencyRequest.new \ + dep, nil + + spec = Gem::DependencyResolver::IndexSpecification.new \ + nil, from_name, from_version, remote, Gem::Platform::RUBY + activation = Gem::DependencyResolver::ActivationRequest.new spec, parent + + Gem::DependencyResolver::DependencyRequest.new dep, activation + end + ## # Constructs a new Gem::Requirement. @@ -1035,7 +1097,11 @@ Also, a list: class StaticSet def initialize(specs) - @specs = specs.sort_by { |s| s.full_name } + @specs = specs + end + + def add spec + @specs << spec end def find_spec(dep) @@ -1048,6 +1114,15 @@ Also, a list: @specs.find_all { |s| dep.matches_spec? s } end + def load_spec name, ver, platform, source + dep = Gem::Dependency.new name, ver + spec = find_spec dep + + Gem::Specification.new spec.name, spec.version do |s| + s.platform = spec.platform + end + end + def prefetch(reqs) end end @@ -1080,18 +1155,18 @@ Also, a list: end ## - # Loads an RSA private key named +key_name+ in test/rubygems/ + # Loads an RSA private key named +key_name+ with +passphrase+ in test/rubygems/ - def self.load_key key_name + def self.load_key key_name, passphrase = nil key_file = key_path key_name key = File.read key_file - OpenSSL::PKey::RSA.new key + OpenSSL::PKey::RSA.new key, passphrase end ## - # Returns the path tot he key named +key_name+ from test/rubygems + # Returns the path to the key named +key_name+ from test/rubygems def self.key_path key_name File.expand_path "../../../test/rubygems/#{key_name}_key.pem", __FILE__ @@ -1100,17 +1175,24 @@ Also, a list: # :stopdoc: # only available in RubyGems tests + PRIVATE_KEY_PASSPHRASE = 'Foo bar' + begin - PRIVATE_KEY = load_key 'private' - PRIVATE_KEY_PATH = key_path 'private' - PUBLIC_KEY = PRIVATE_KEY.public_key + PRIVATE_KEY = load_key 'private' + PRIVATE_KEY_PATH = key_path 'private' - PUBLIC_CERT = load_cert 'public' - PUBLIC_CERT_PATH = cert_path 'public' + # ENCRYPTED_PRIVATE_KEY is PRIVATE_KEY encrypted with PRIVATE_KEY_PASSPHRASE + ENCRYPTED_PRIVATE_KEY = load_key 'encrypted_private', PRIVATE_KEY_PASSPHRASE + ENCRYPTED_PRIVATE_KEY_PATH = key_path 'encrypted_private' + + PUBLIC_KEY = PRIVATE_KEY.public_key + + PUBLIC_CERT = load_cert 'public' + PUBLIC_CERT_PATH = cert_path 'public' rescue Errno::ENOENT PRIVATE_KEY = nil PUBLIC_KEY = nil PUBLIC_CERT = nil - end + end if defined?(OpenSSL::SSL) end diff --git a/lib/rubygems/uninstaller.rb b/lib/rubygems/uninstaller.rb index d672b9dec1..143ab6df26 100644 --- a/lib/rubygems/uninstaller.rb +++ b/lib/rubygems/uninstaller.rb @@ -43,14 +43,15 @@ class Gem::Uninstaller 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) - @force_executables = options[:executables] - @force_all = options[:all] - @force_ignore = options[:ignore] - @bin_dir = options[:bin_dir] - @format_executable = options[:format_executable] + @gem = gem + @version = options[:version] || Gem::Requirement.default + @gem_home = File.expand_path(options[:install_dir] || Gem.dir) + @force_executables = options[:executables] + @force_all = options[:all] + @force_ignore = options[:ignore] + @bin_dir = options[:bin_dir] + @format_executable = options[:format_executable] + @abort_on_dependent = options[:abort_on_dependent] # Indicate if development dependencies should be checked when # uninstalling. (default: false) @@ -143,7 +144,7 @@ class Gem::Uninstaller @spec = spec unless dependencies_ok? spec - unless ask_if_ok(spec) + if abort_on_dependent? || !ask_if_ok(spec) raise Gem::DependencyRemovalException, "Uninstallation aborted due to dependent gem(s)" end @@ -290,6 +291,10 @@ class Gem::Uninstaller deplist.ok_to_remove?(spec.full_name, @check_dev) end + def abort_on_dependent? + @abort_on_dependent + end + def ask_if_ok(spec) msg = [''] msg << 'You have requested to uninstall the gem:' diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb index bbf04f5403..2e546462d4 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -147,13 +147,16 @@ class Gem::Version # FIX: These are only used once, in .correct?. Do they deserve to be # constants? - VERSION_PATTERN = '[0-9]+(?>\.[0-9a-zA-Z]+)*' # :nodoc: + VERSION_PATTERN = '[0-9]+(?>\.[0-9a-zA-Z]+)*(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?' # :nodoc: ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})*\s*\z/ # :nodoc: ## # A string representation of this Version. - attr_reader :version + def version + @version.dup + end + alias to_s version ## @@ -183,6 +186,12 @@ class Gem::Version end end + @@all = {} + + def self.new version + @@all[version] ||= super + end + ## # Constructs a Version from the +version+ string. A version string is a # series of digits or ASCII letters separated by dots. @@ -191,7 +200,8 @@ class Gem::Version raise ArgumentError, "Malformed version number string #{version}" unless self.class.correct?(version) - @version = version.to_s.dup.strip + @version = version.to_s.strip.gsub("-",".pre.") + @segments = nil end ## diff --git a/lib/rubygems/version_option.rb b/lib/rubygems/version_option.rb index a3de4dc9e7..a0755d5020 100644 --- a/lib/rubygems/version_option.rb +++ b/lib/rubygems/version_option.rb @@ -42,6 +42,7 @@ module Gem::VersionOption add_option("--[no-]prerelease", "Allow prerelease versions of a gem", *wrap) do |value, options| options[:prerelease] = value + options[:explicit_prerelease] = true end end @@ -50,14 +51,19 @@ module Gem::VersionOption def add_version_option(task = command, *wrap) OptionParser.accept Gem::Requirement do |value| - Gem::Requirement.new value + Gem::Requirement.new(*value.split(/\s*,\s*/)) end add_option('-v', '--version VERSION', Gem::Requirement, "Specify version of gem to #{task}", *wrap) do |value, options| options[:version] = value - options[:prerelease] = true if value.prerelease? + + explicit_prerelease_set = !options[:explicit_prerelease].nil? + options[:explicit_prerelease] = false unless explicit_prerelease_set + + options[:prerelease] = value.prerelease? unless + options[:explicit_prerelease] end end -- cgit v1.2.3