diff --git a/ChangeLog b/ChangeLog index 0f1bd4f9db..e07f69817f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +Wed Jul 10 08:21:15 2013 Eric Hodel + + * lib/rubygems: Import RubyGems 2.1 + * test/rubygems: Ditto. + Wed Jul 10 07:34:34 2013 Eric Hodel * lib/rubygems/ext/ext_conf_builder.rb: Remove siteconf file after diff --git a/lib/rubygems.rb b/lib/rubygems.rb index 2c8dbee2a2..dfc1006fd4 100644 --- a/lib/rubygems.rb +++ b/lib/rubygems.rb @@ -8,7 +8,7 @@ require 'rbconfig' module Gem - VERSION = '2.0.4' + VERSION = '2.1.0' end # Must be first since it unloads the prelude from 1.9.2 @@ -143,6 +143,14 @@ module Gem specifications ] + ## + # Subdirectories in a gem repository for default gems + + REPOSITORY_DEFAULT_GEM_SUBDIRECTORIES = %w[ + gems + specifications/default + ] + @@win_platform = nil @configuration = nil @@ -379,6 +387,10 @@ module Gem paths.path end + def self.spec_cache_dir + paths.spec_cache_dir + end + ## # Quietly ensure the Gem directory +dir+ contains all the proper # subdirectories. If we can't create a directory due to a permission @@ -389,6 +401,23 @@ module Gem # World-writable directories will never be created. def self.ensure_gem_subdirectories dir = Gem.dir, mode = nil + ensure_subdirectories(dir, mode, REPOSITORY_SUBDIRECTORIES) + end + + ## + # Quietly ensure the Gem directory +dir+ contains all the proper + # subdirectories for handling default gems. If we can't create a + # directory due to a permission problem, then we will silently continue. + # + # If +mode+ is given, missing directories are created with this mode. + # + # World-writable directories will never be created. + + def self.ensure_default_gem_subdirectories dir = Gem.dir, mode = nil + ensure_subdirectories(dir, mode, REPOSITORY_DEFAULT_GEM_SUBDIRECTORIES) + end + + def self.ensure_subdirectories dir, mode, subdirs # :nodoc: old_umask = File.umask File.umask old_umask | 002 @@ -398,7 +427,7 @@ module Gem options[:mode] = mode if mode - REPOSITORY_SUBDIRECTORIES.each do |name| + subdirs.each do |name| subdir = File.join dir, name next if File.exist? subdir FileUtils.mkdir_p subdir, options rescue nil @@ -971,10 +1000,33 @@ module Gem attr_reader :loaded_specs ## - # Register a Gem::Specification for default gem + # Register a Gem::Specification for default gem. + # + # Two formats for the specification are supported: + # + # * MRI 2.0 style, where spec.files contains unprefixed require names. + # The spec's filenames will be registered as-is. + # * New style, where spec.files contains files prefixed with paths + # from spec.require_paths. The prefixes are stripped before + # registering the spec's filenames. Unprefixed files are omitted. + # def register_default_spec(spec) + new_format, prefix_pattern = nil + spec.files.each do |file| + if new_format == nil + new_format = spec.require_paths.any? {|path| file.start_with? path} + + prefix_group = spec.require_paths.map {|f| f + "/"}.join("|") + prefix_pattern = /^(#{prefix_group})/ + end + + if new_format + file = file.sub(prefix_pattern, "") + next unless $~ + end + @path_to_default_spec_map[file] = spec end end 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/basic_specification.rb b/lib/rubygems/basic_specification.rb new file mode 100644 index 0000000000..2e47f32986 --- /dev/null +++ b/lib/rubygems/basic_specification.rb @@ -0,0 +1,139 @@ +module Gem + # BasicSpecification is an abstract class which implements some common code used by + # both Specification and StubSpecification. + class BasicSpecification + def self.default_specifications_dir + File.join(Gem.default_dir, "specifications", "default") + end + + ## + # Name of the gem + + def name + raise NotImplementedError + end + + ## + # Version of the gem + + def version + raise NotImplementedError + end + + ## + # Platform of the gem + + def platform + raise NotImplementedError + end + + ## + # Require paths of the gem + + def require_paths + raise NotImplementedError + end + + ## + # True when the gem has been activated + + def activated? + raise NotImplementedError + end + + ## + # Return a Gem::Specification from this gem + + def to_spec + raise NotImplementedError + end + + ## + # The filename of the gem specification + attr_reader :filename + + ## + # Set the filename of the Specification was loaded from. +path+ is converted + # to a String. + + def filename= path + @filename = path && path.to_s + + @full_gem_path = nil + @gems_dir = nil + @base_dir = nil + 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 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. + @full_gem_path ||= find_full_gem_path + end + + # :nodoc: + def find_full_gem_path + # TODO: also, shouldn't it default to full_name if it hasn't been written? + path = File.expand_path File.join(gems_dir, full_name) + path.untaint + path if File.directory? path + end + private :find_full_gem_path + + ## + # 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(filename && base_dir || Gem.dir, "gems") + 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 filename + @base_dir ||= if default_gem? then + File.dirname File.dirname File.dirname filename + else + File.dirname File.dirname filename + end + end + + def default_gem? + filename && + File.dirname(filename) == self.class.default_specifications_dir + 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. + + def full_name + if platform == Gem::Platform::RUBY or platform.nil? then + "#{name}-#{version}".untaint + else + "#{name}-#{version}-#{platform}".untaint + end + end + end +end diff --git a/lib/rubygems/commands/cert_command.rb b/lib/rubygems/commands/cert_command.rb index 5a9320f9c4..99e9690755 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 @@ -115,16 +121,31 @@ class Gem::Commands::CertCommand < Gem::Command end def build name - key = options[:key] || Gem::Security.create_key + if options[:key] + key = options[:key] + else + passphrase = ask_for_password 'Passphrase for your Private Key:' + say "\n" - cert = Gem::Security.create_cert_email name, key + passphrase_confirmation = ask_for_password 'Please repeat the passphrase for your Private Key:' + say "\n" - key_path = Gem::Security.write key, "gem-private_key.pem" + 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 + end + + cert = Gem::Security.create_cert_email name, key cert_path = Gem::Security.write cert, "gem-public_cert.pem" say "Certificate: #{cert_path}" - say "Private Key: #{key_path}" - say "Don't forget to move the key file to somewhere private!" + + if key_path + say "Private Key: #{key_path}" + say "Don't forget to move the key file to somewhere private!" + end end def certificates_matching filter @@ -198,7 +219,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" @@ -225,5 +247,5 @@ For further reading on signing gems see `ri Gem::Security`. Gem::Security.write cert, cert_file, permissions end -end +end if defined?(OpenSSL::SSL) diff --git a/lib/rubygems/commands/cleanup_command.rb b/lib/rubygems/commands/cleanup_command.rb index 61f189e449..cacfe89404 100644 --- a/lib/rubygems/commands/cleanup_command.rb +++ b/lib/rubygems/commands/cleanup_command.rb @@ -9,7 +9,8 @@ class Gem::Commands::CleanupCommand < Gem::Command 'Clean up old versions of installed gems in the local repository', :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 @@ -162,4 +163,3 @@ are not removed. end end - diff --git a/lib/rubygems/commands/environment_command.rb b/lib/rubygems/commands/environment_command.rb index 40e71cf094..39d52e7f01 100644 --- a/lib/rubygems/commands/environment_command.rb +++ b/lib/rubygems/commands/environment_command.rb @@ -99,6 +99,8 @@ lib/rubygems/defaults/operating_system.rb out << " - EXECUTABLE DIRECTORY: #{Gem.bindir}\n" + out << " - SPEC CACHE DIRECTORY: #{Gem.spec_cache_dir}\n" + out << " - RUBYGEMS PLATFORMS:\n" Gem.platforms.each do |platform| out << " - #{platform}\n" @@ -107,11 +109,9 @@ lib/rubygems/defaults/operating_system.rb out << " - GEM PATHS:\n" out << " - #{Gem.dir}\n" - path = Gem.path.dup - path.delete Gem.dir - path.each do |p| - out << " - #{p}\n" - end + 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| @@ -124,6 +124,11 @@ lib/rubygems/defaults/operating_system.rb out << " - #{s}\n" end + out << " - SHELL PATH:\n" + + shell_path = ENV['PATH'].split(File::PATH_SEPARATOR) + add_path out, shell_path + else raise Gem::CommandLineError, "Unknown environment option [#{arg}]" end @@ -131,5 +136,11 @@ lib/rubygems/defaults/operating_system.rb true end + def add_path out, path + path.each do |component| + out << " - #{component}\n" + end + end + end diff --git a/lib/rubygems/commands/help_command.rb b/lib/rubygems/commands/help_command.rb index 7f1fb486e0..9f0ebc087f 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 @@ -119,7 +133,7 @@ platform. if command then command.summary else - "[No command found for #{cmd_name}, bug?]" + "[No command found for #{cmd_name}]" end summary = wrap(summary, summary_width).split "\n" diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb index 0b58fa665e..ac78764af5 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 @@ -39,6 +37,12 @@ class Gem::Commands::InstallCommand < Gem::Command 'install the listed gems') do |v,o| 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 @@ -153,7 +157,14 @@ to write the specification by hand. For example: alert_error "Can't use --version w/ multiple gems. Use name:ver instead." terminate_interaction 1 end - + + # load post-install hooks appropriate to options + if options[:install_as_default] + require 'rubygems/install_default_message' + else + require 'rubygems/install_message' + end + require 'rubygems/rdoc' get_all_gem_names_and_versions.each do |gem_name, gem_version| gem_version ||= options[:version] diff --git a/lib/rubygems/commands/owner_command.rb b/lib/rubygems/commands/owner_command.rb index 11e6e026fd..c1c0a570d6 100644 --- a/lib/rubygems/commands/owner_command.rb +++ b/lib/rubygems/commands/owner_command.rb @@ -31,9 +31,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 f22fe11769..0a7f315a0a 100644 --- a/lib/rubygems/commands/pristine_command.rb +++ b/lib/rubygems/commands/pristine_command.rb @@ -30,6 +30,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 @@ -104,16 +110,21 @@ with extensions. 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/sources_command.rb b/lib/rubygems/commands/sources_command.rb index f4cc3e57ae..82bb1f62eb 100644 --- a/lib/rubygems/commands/sources_command.rb +++ b/lib/rubygems/commands/sources_command.rb @@ -48,7 +48,7 @@ class Gem::Commands::SourcesCommand < Gem::Command options[:update]) if options[:clear_all] then - path = File.join Gem.user_home, '.gem', 'specs' + path = Gem.spec_cache_dir FileUtils.rm_rf path unless File.exist? path then diff --git a/lib/rubygems/commands/uninstall_command.rb b/lib/rubygems/commands/uninstall_command.rb index 56aa8ee57f..68d170acb1 100644 --- a/lib/rubygems/commands/uninstall_command.rb +++ b/lib/rubygems/commands/uninstall_command.rb @@ -67,6 +67,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 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 71062410cb..b9172e26c0 100755 --- a/lib/rubygems/core_ext/kernel_require.rb +++ b/lib/rubygems/core_ext/kernel_require.rb @@ -57,7 +57,7 @@ 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 } diff --git a/lib/rubygems/defaults.rb b/lib/rubygems/defaults.rb index cc8dc722fc..9b5bea21da 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 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..2523d1b2ae 100644 --- a/lib/rubygems/dependency_installer.rb +++ b/lib/rubygems/dependency_installer.rb @@ -1,11 +1,12 @@ 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/local' +require 'rubygems/source/specific_file' require 'rubygems/available_set' ## @@ -15,15 +16,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 +28,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 +71,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 +98,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,128 +110,14 @@ 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 - ## - # Creates an AvailableSet to install from based on +dep_or_name+ and - # +version+ + #-- + # TODO remove, no longer used - def available_set_for dep_or_name, version # :nodoc: - if String === dep_or_name then - find_spec_by_name_and_version dep_or_name, version, @prerelease - else - dep = dep_or_name.dup - dep.prerelease = @prerelease - @available = find_gems_with_sources dep - end - - @available.pick_best! - end - - ## - # Indicated, based on the requested domain, if local - # gems should be considered. - - def consider_local? - @domain == :both or @domain == :local - end - - ## - # Indicated, based on the requested domain, if remote - # gems should be considered. - - def consider_remote? - @domain == :both or @domain == :remote - end - - ## - # Returns a list of pairs of gemspecs and source_uris that match - # Gem::Dependency +dep+ from both local (Dir.pwd) and remote (Gem.sources) - # sources. Gems are sorted with newer gems preferred over older gems, and - # local gems preferred over remote gems. - - def find_gems_with_sources(dep) - set = Gem::AvailableSet.new - - if consider_local? - sl = Gem::Source::Local.new - - if spec = sl.find_gem(dep.name) - if dep.matches_spec? spec - set.add spec, sl - end - end - end - - if consider_remote? - begin - found, errors = Gem::SpecFetcher.fetcher.spec_for_dependency dep - - if @errors - @errors += errors - else - @errors = errors - end - - set << found - - rescue Gem::RemoteFetcher::FetchError => e - # FIX if there is a problem talking to the network, we either need to always tell - # the user (no really_verbose) or fail hard, not silently tell them that we just - # couldn't find their requested gem. - if Gem.configuration.really_verbose then - say "Error fetching remote data:\t\t#{e.message}" - say "Falling back to local-only install" - end - @domain = :local - end - end - - set - end - - ## - # Gathers all dependencies necessary for the installation from local and - # remote sources unless the ignore_dependencies was given. - - def gather_dependencies - specs = @available.all_specs - - # these gems were listed by the user, always install them - keep_names = specs.map { |spec| spec.full_name } - - if @dev_shallow - @toplevel_specs = keep_names - end - - dependency_list = Gem::DependencyList.new @development - dependency_list.add(*specs) - to_do = specs.dup - add_found_dependencies to_do, dependency_list unless @ignore_dependencies - - # REFACTOR maybe abstract away using Gem::Specification.include? so - # that this isn't dependent only on the currently installed gems - dependency_list.specs.reject! { |spec| - not keep_names.include?(spec.full_name) and - Gem::Specification.include?(spec) - } - - unless dependency_list.ok? or @ignore_dependencies or @force then - reason = dependency_list.why_not_ok?.map { |k,v| - "#{k} requires #{v.join(", ")}" - }.join("; ") - raise Gem::DependencyError, "Unable to resolve dependencies: #{reason}" - end - - @gems_to_install = dependency_list.dependency_order.reverse - end - - def add_found_dependencies to_do, dependency_list + def add_found_dependencies to_do, dependency_list # :nodoc: seen = {} dependencies = Hash.new { |h, name| h[name] = Gem::Dependency.new name } @@ -262,15 +165,92 @@ class Gem::DependencyInstaller dependency_list.remove_specs_unsatisfied_by dependencies end + ## + # Creates an AvailableSet to install from based on +dep_or_name+ and + # +version+ + + def available_set_for dep_or_name, version # :nodoc: + if String === dep_or_name then + find_spec_by_name_and_version dep_or_name, version, @prerelease + else + dep = dep_or_name.dup + dep.prerelease = @prerelease + @available = find_gems_with_sources dep + end + + @available.pick_best! + end + + ## + # Indicated, based on the requested domain, if local + # gems should be considered. + + def consider_local? + @domain == :both or @domain == :local + end + + ## + # Indicated, based on the requested domain, if remote + # gems should be considered. + + def consider_remote? + @domain == :both or @domain == :remote + end + + ## + # Returns a list of pairs of gemspecs and source_uris that match + # Gem::Dependency +dep+ from both local (Dir.pwd) and remote (Gem.sources) + # sources. Gems are sorted with newer gems preferred over older gems, and + # local gems preferred over remote gems. + + def find_gems_with_sources dep # :nodoc: + set = Gem::AvailableSet.new + + if consider_local? + sl = Gem::Source::Local.new + + if spec = sl.find_gem(dep.name) + if dep.matches_spec? spec + set.add spec, sl + end + end + end + + if consider_remote? + begin + found, errors = Gem::SpecFetcher.fetcher.spec_for_dependency dep + + if @errors + @errors += errors + else + @errors = errors + end + + set << found + + rescue Gem::RemoteFetcher::FetchError => e + # FIX if there is a problem talking to the network, we either need to always tell + # the user (no really_verbose) or fail hard, not silently tell them that we just + # couldn't find their requested gem. + if Gem.configuration.really_verbose then + say "Error fetching remote data:\t\t#{e.message}" + say "Falling back to local-only install" + end + @domain = :local + end + end + + 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, + def find_spec_by_name_and_version gem_name, version = Gem::Requirement.default, - prerelease = false) + prerelease = false set = Gem::AvailableSet.new @@ -303,6 +283,59 @@ class Gem::DependencyInstaller @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 # :nodoc: + specs = @available.all_specs + + # these gems were listed by the user, always install them + keep_names = specs.map { |spec| spec.full_name } + + if @dev_shallow + @toplevel_specs = keep_names + end + + dependency_list = Gem::DependencyList.new @development + dependency_list.add(*specs) + to_do = specs.dup + add_found_dependencies to_do, dependency_list unless @ignore_dependencies + + # REFACTOR maybe abstract away using Gem::Specification.include? so + # that this isn't dependent only on the currently installed gems + dependency_list.specs.reject! { |spec| + not keep_names.include?(spec.full_name) and + Gem::Specification.include?(spec) + } + + unless dependency_list.ok? or @ignore_dependencies or @force then + reason = dependency_list.why_not_ok?.map { |k,v| + "#{k} requires #{v.join(", ")}" + }.join("; ") + raise Gem::DependencyError, "Unable to resolve dependencies: #{reason}" + end + + @gems_to_install = dependency_list.dependency_order.reverse + end + + 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 + yield unless fork_happened + end + ## # Installs the gem +dep_or_name+ and all its dependencies. Returns an Array # of installed gem specifications. @@ -318,61 +351,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 + 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 - # 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 - - 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 +387,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..e5c05972d8 100644 --- a/lib/rubygems/dependency_resolver.rb +++ b/lib/rubygems/dependency_resolver.rb @@ -1,575 +1,240 @@ 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 + attr_accessor :development + + attr_reader :missing + + ## + # When a missing dependency, don't stop. Just go on and record what was + # missing. + + attr_accessor :soft_missing + + def self.compose_sets *sets + Gem::DependencyResolver::ComposedSet.new(*sets) end - # Raised when a dependency requests a gem for which there is - # no spec. - # - class UnsatisfiableDepedencyError < Gem::Exception - def initialize(dep) - super "unable to find any gem matching dependency '#{dep}'" + ## + # Provide a DependencyResolver that queries only against the already + # installed gems. - @dependency = dep - end - - attr_reader :dependency + def self.for_current_gems needed + new needed, Gem::DependencyResolver::CurrentSet.new end - # Raised when dependencies conflict and create the inability to - # find a valid possible spec for a request. + ## + # Create DependencyResolver object which will resolve the tree starting + # with +needed+ Depedency objects. # - 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 + # +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 dependency - @request.dependency - end + def initialize needed, set = nil + @set = set || Gem::DependencyResolver::IndexSet.new + @needed = needed - attr_reader :conflicts + @conflicts = nil + @development = false + @missing = [] + @soft_missing = false end - # Given a set of Gem::Dependency objects as +needed+ and a way - # to query the set of available specs via +set+, calculates - # a set of ActivationRequest objects which indicate all the specs - # that should be activated to meet the all the requirements. - # - class DependencyResolver - - # Represents a specification retrieved via the rubygems.org - # API. This is used to avoid having to load the full - # Specification object when all we need is the name, version, - # and dependencies. - # - class APISpecification - 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 + 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 - # 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 + @set.prefetch reqs - # Return data for all versions of the gem +name+. - # - def versions(name) - if @data.key?(name) - return @data[name] - end + reqs + end - uri = @dep_uri + "?gems=#{name}" - str = Gem::RemoteFetcher.fetcher.fetch_path uri + ## + # Proceed with resolution! Returns an array of ActivationRequest objects. - Marshal.load(str).each do |ver| - @data[ver[:name]] << ver - end + def resolve + @conflicts = [] - @data[name] - end + needed = nil - # 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 + @needed.reverse_each do |n| + needed = Gem::List.new(Gem::DependencyResolver::DependencyRequest.new(n, nil), needed) 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 + res = resolve_for needed, nil - @spec = nil - end + raise Gem::DependencyResolutionError, res if + res.kind_of? Gem::DependencyResolver::DependencyConflict - attr_reader :name, :version, :source + res.to_a + end - def full_name - "#{@name}-#{@version}" - 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 spec - @spec ||= @set.load_spec(@name, @version, @source) - end + def resolve_for needed, specs + while needed + dep = needed.value + needed = needed.tail - def dependencies - spec.dependencies - end - end + # If there is already a spec activated for the requested name... + if specs && existing = specs.find { |s| dep.name == s.name } - # The global rubygems pool represented via the traditional - # source index. - # - class IndexSet - def initialize - @f = Gem::SpecFetcher.fetcher + # then we're done since this new dep matches the + # existing spec. + next if dep.matches_spec? existing - @all = Hash.new { |h,k| h[k] = [] } + # There is a conflict! We return the conflict + # object which will be seen by the caller and be + # handled at the right level. - list, _ = @f.available_specs(:released) - list.each do |uri, specs| - specs.each do |n| - @all[n.name] << [uri, n] - end - end - - @specs = {} - end - - # Return an array of IndexSpecification objects matching - # DependencyRequest +req+. - # - def find_all(req) - res = [] - - name = req.dependency.name - - @all[name].each do |uri, n| - if req.dependency.match? n - res << IndexSpecification.new(self, n.name, n.version, - uri, n.platform) - end - end - - res - end - - # No prefetching needed since we load the whole index in - # initially. - # - def prefetch(gems) - end - - # Called from IndexSpecification to get a true Specification - # object. - # - def load_spec(name, ver, source) - key = "#{name}-#{ver}" - @specs[key] ||= source.fetch_spec(Gem::NameTuple.new(name, ver)) - end - end - - # A set which represents the installed gems. Respects - # all the normal settings that control where to look - # for installed gems. - # - class CurrentSet - def find_all(req) - req.dependency.matching_specs - end - - def prefetch(gems) - end - end - - # Create DependencyResolver object which will resolve - # the tree starting with +needed+ Depedency objects. - # - # +set+ is an object that provides where to look for - # specifications to satisify the Dependencies. This - # defaults to IndexSet, which will query rubygems.org. - # - def initialize(needed, set=IndexSet.new) - @set = set || IndexSet.new # Allow nil to mean IndexSet - @needed = needed - - @conflicts = nil - end - - # Provide a DependencyResolver that queries only against - # the already installed gems. - # - def self.for_current_gems(needed) - new needed, CurrentSet.new - end - - # Contains all the conflicts encountered while doing resolution - # - attr_reader :conflicts - - # Proceed with resolution! Returns an array of ActivationRequest - # objects. - # - def resolve - @conflicts = [] - - needed = @needed.map { |n| DependencyRequest.new(n, nil) } - - res = resolve_for needed, [] - - if res.kind_of? DependencyConflict - raise DependencyResolutionError.new(res) - end - - res - end - - # Used internally to indicate that a dependency conflicted - # with a spec that would be activated. - # - class DependencyConflict - def initialize(dependency, activated, failed_dep=dependency) - @dependency = dependency - @activated = activated - @failed_dep = failed_dep - end - - attr_reader :dependency, :activated - - # Return the Specification that listed the dependency - # - def requester - @failed_dep.requester - end - - def for_spec?(spec) - @dependency.name == spec.name - end - - # Return the 2 dependency objects that conflicted - # - def conflicting_dependencies - [@failed_dep.dependency, @activated.request.dependency] - end - end - - # Used Internally. Wraps a Depedency object to also track - # which spec contained the Dependency. - # - class DependencyRequest - def initialize(dep, act) - @dependency = dep - @requester = act - end - - attr_reader :dependency, :requester - - def name - @dependency.name - end - - def matches_spec?(spec) - @dependency.matches_spec? spec - end - - def to_s - @dependency.to_s - end - - def ==(other) - case other - when Dependency - @dependency == other - when DependencyRequest - @dependency == other.dependency && @requester == other.requester + # 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 + @conflicts << conflict - # 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 + return conflict end - attr_reader :spec, :request + # Get a list of all specs that satisfy dep + possible = @set.find_all dep - # Indicate if this activation is one of a set of possible - # requests for the same Dependency request. - # - def others_possible? - @others_possible - end + case possible.size + when 0 + @missing << dep - # Return the ActivationRequest that contained the dependency - # that we were activated for. - # - def parent - @request.requester - end - - def name - @spec.name - end - - def full_name - @spec.full_name - end - - def version - @spec.version - end - - def full_spec - Gem::Specification === @spec ? @spec : @spec.spec - end - - def download(path) - if @spec.respond_to? :source - source = @spec.source - else - source = Gem.sources.first - end - - Gem.ensure_gem_subdirectories path - - source.download full_spec, path - end - - def ==(other) - case other - when Gem::Specification - @spec == other - when ActivationRequest - @spec == other.spec && @request == other.request - else - false - end - end - - ## - # Indicates if the requested gem has already been installed. - - def installed? - this_spec = full_spec - - Gem::Specification.any? do |s| - s == this_spec - end - end - end - - def requests(s, act) - reqs = [] - s.dependencies.each do |d| - next unless d.type == :runtime - reqs << DependencyRequest.new(d, act) - end - - @set.prefetch(reqs) - - reqs - end - - # The meat of the algorithm. Given +needed+ DependencyRequest objects - # and +specs+ being a list to ActivationRequest, calculate a new list - # of ActivationRequest objects. - # - def resolve_for(needed, specs) - until needed.empty? - dep = needed.shift - - # If there is already a spec activated for the requested name... - if existing = specs.find { |s| dep.name == s.name } - - # then we're done since this new dep matches the - # existing spec. - next if dep.matches_spec? existing - - # There is a conflict! We return the conflict - # object which will be seen by the caller and be - # handled at the right level. - - # If the existing activation indicates that there - # are other possibles for it, then issue the conflict - # on the dep for the activation itself. Otherwise, issue - # it on the requester's request itself. - # - if existing.others_possible? - conflict = DependencyConflict.new(dep, existing) - else - depreq = existing.request.requester.request - conflict = DependencyConflict.new(depreq, existing, dep) - end - @conflicts << conflict - - return conflict - end - - # Get a list of all specs that satisfy dep - possible = @set.find_all(dep) - - case possible.size - when 0 + unless @soft_missing # If there are none, then our work here is done. - raise UnsatisfiableDepedencyError.new(dep) - when 1 - # If there is one, then we just add it to specs - # and process the specs dependencies by adding - # them to needed. - - spec = possible.first - act = ActivationRequest.new(spec, dep, false) - - specs << act - - # Put the deps for at the beginning of needed - # rather than the end to match the depth first - # searching done by the multiple case code below. - # - # This keeps the error messages consistent. - needed = requests(spec, act) + needed - else - # There are multiple specs for this dep. This is - # the case that this class is built to handle. - - # Sort them so that we try the highest versions - # first. - possible = possible.sort_by { |s| s.version } - - # We track the conflicts seen so that we can report them - # to help the user figure out how to fix the situation. - conflicts = [] - - # To figure out which to pick, we keep resolving - # given each one being activated and if there isn't - # a conflict, we know we've found a full set. - # - # We use an until loop rather than #reverse_each - # to keep the stack short since we're using a recursive - # algorithm. - # - until possible.empty? - s = possible.pop - - # Recursively call #resolve_for with this spec - # and add it's dependencies into the picture... - - act = ActivationRequest.new(s, dep) - - try = requests(s, act) + needed - - res = resolve_for(try, specs + [act]) - - # While trying to resolve these dependencies, there may - # be a conflict! - - if res.kind_of? DependencyConflict - # The conflict might be created not by this invocation - # but rather one up the stack, so if we can't attempt - # to resolve this conflict (conflict isn't with the spec +s+) - # then just return it so the caller can try to sort it out. - return res unless res.for_spec? s - - # Otherwise, this is a conflict that we can attempt to fix - conflicts << [s, res] - - # Optimization: - # - # Because the conflict indicates the dependency that trigger - # it, we can prune possible based on this new information. - # - # This cuts down on the number of iterations needed. - possible.delete_if { |x| !res.dependency.matches_spec? x } - else - # No conflict, return the specs - return res - end - end - - # We tried all possibles and nothing worked, so we let the user - # know and include as much information about the problem since - # the user is going to have to take action to fix this. - raise ImpossibleDependenciesError.new(dep, conflicts) + raise Gem::UnsatisfiableDependencyError, dep end - end + when 1 + # If there is one, then we just add it to specs + # and process the specs dependencies by adding + # them to needed. - specs + 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 { |s| [s.source, 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 = 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 + # No conflict, return the specs + return res + end + end + + # We tried all possibles and nothing worked, so we let the user + # know and include as much information about the problem since + # the user is going to have to take action to fix this. + raise Gem::ImpossibleDependenciesError.new(dep, conflicts) + end end + + specs 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/dependency_resolver/activation_request.rb b/lib/rubygems/dependency_resolver/activation_request.rb new file mode 100644 index 0000000000..25af6378ac --- /dev/null +++ b/lib/rubygems/dependency_resolver/activation_request.rb @@ -0,0 +1,109 @@ +## +# Specifies a Specification object that should be activated. +# Also contains a dependency that was used to introduce this +# activation. + +class Gem::DependencyResolver::ActivationRequest + + attr_reader :request + + attr_reader :spec + + def initialize spec, req, others_possible = true + @spec = spec + @request = req + @others_possible = others_possible + end + + def == other + case other + when Gem::Specification + @spec == other + when Gem::DependencyResolver::ActivationRequest + @spec == other.spec && @request == other.request + else + false + end + end + + def download path + if @spec.respond_to? :source + source = @spec.source + else + source = Gem.sources.first + end + + Gem.ensure_gem_subdirectories path + + source.download full_spec, path + end + + def full_name + @spec.full_name + end + + def full_spec + Gem::Specification === @spec ? @spec : @spec.spec + end + + def inspect # :nodoc: + others_possible = nil + others_possible = ' (others possible)' if @others_possible + + '#<%s for %p from %s%s>' % [ + self.class, @spec, @request, others_possible + ] + end + + ## + # Indicates if the requested gem has already been installed. + + def installed? + this_spec = full_spec + + Gem::Specification.any? do |s| + s == this_spec + end + end + + def name + @spec.name + end + + ## + # Indicate if this activation is one of a set of possible + # requests for the same Dependency request. + + def others_possible? + @others_possible + end + + ## + # Return the ActivationRequest that contained the dependency + # that we were activated for. + + def parent + @request.requester + end + + def pretty_print q # :nodoc: + q.group 2, '[Activation request', ']' do + q.breakable + q.pp @spec + + q.breakable + q.text ' for ' + q.pp @request + + + q.breakable + q.text ' (other possible)' if @others_possible + end + end + + def version + @spec.version + end + +end + diff --git a/lib/rubygems/dependency_resolver/api_set.rb b/lib/rubygems/dependency_resolver/api_set.rb new file mode 100644 index 0000000000..469c005a09 --- /dev/null +++ b/lib/rubygems/dependency_resolver/api_set.rb @@ -0,0 +1,65 @@ +## +# The global rubygems pool, available via the rubygems.org API. +# Returns instances of APISpecification. + +class Gem::DependencyResolver::APISet + + def initialize + @data = Hash.new { |h,k| h[k] = [] } + @dep_uri = URI 'https://rubygems.org/api/v1/dependencies' + 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 << Gem::DependencyResolver::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 + + ## + # 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 + +end + diff --git a/lib/rubygems/dependency_resolver/api_specification.rb b/lib/rubygems/dependency_resolver/api_specification.rb new file mode 100644 index 0000000000..5ad07396cf --- /dev/null +++ b/lib/rubygems/dependency_resolver/api_specification.rb @@ -0,0 +1,36 @@ +## +# 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 Gem::DependencyResolver::APISpecification + + attr_reader :dependencies + attr_reader :name + attr_reader :set # :nodoc: + attr_reader :version + + 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 + + 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 + diff --git a/lib/rubygems/dependency_resolver/composed_set.rb b/lib/rubygems/dependency_resolver/composed_set.rb new file mode 100644 index 0000000000..fb38128bb0 --- /dev/null +++ b/lib/rubygems/dependency_resolver/composed_set.rb @@ -0,0 +1,18 @@ +class Gem::DependencyResolver::ComposedSet + + def initialize *sets + @sets = sets + end + + def find_all req + res = [] + @sets.each { |s| res += s.find_all(req) } + res + end + + def prefetch reqs + @sets.each { |s| s.prefetch(reqs) } + end + +end + diff --git a/lib/rubygems/dependency_resolver/current_set.rb b/lib/rubygems/dependency_resolver/current_set.rb new file mode 100644 index 0000000000..13bc490e9e --- /dev/null +++ b/lib/rubygems/dependency_resolver/current_set.rb @@ -0,0 +1,16 @@ +## +# A set which represents the installed gems. Respects +# all the normal settings that control where to look +# for installed gems. + +class Gem::DependencyResolver::CurrentSet + + def find_all req + req.dependency.matching_specs + end + + def prefetch gems + end + +end + diff --git a/lib/rubygems/dependency_resolver/dependency_conflict.rb b/lib/rubygems/dependency_resolver/dependency_conflict.rb new file mode 100644 index 0000000000..1755d910c3 --- /dev/null +++ b/lib/rubygems/dependency_resolver/dependency_conflict.rb @@ -0,0 +1,85 @@ +## +# Used internally to indicate that a dependency conflicted +# with a spec that would be activated. + +class Gem::DependencyResolver::DependencyConflict + + attr_reader :activated + + attr_reader :dependency + + def initialize(dependency, activated, failed_dep=dependency) + @dependency = dependency + @activated = activated + @failed_dep = failed_dep + end + + ## + # Return the 2 dependency objects that conflicted + + def conflicting_dependencies + [@failed_dep.dependency, @activated.request.dependency] + end + + ## + # Explanation of the conflict used by exceptions to print useful messages + + def explanation + activated = @activated.spec.full_name + requirement = @failed_dep.dependency.requirement + + " Activated %s instead of (%s) via:\n %s\n" % [ + activated, requirement, request_path.join(', ') + ] + end + + def for_spec?(spec) + @dependency.name == spec.name + end + + def pretty_print q # :nodoc: + q.group 2, '[Dependency conflict: ', ']' do + q.breakable + + q.text 'activated ' + q.pp @activated + + q.breakable + q.text ' dependency ' + q.pp @dependency + + q.breakable + if @dependency == @failed_dep then + q.text ' failed' + else + q.text ' failed dependency ' + q.pp @failed_dep + end + end + end + + ## + # Path of specifications that requested this dependency + + def request_path + current = requester + path = [] + + while current do + path << current.spec.full_name + + current = current.request.requester + end + + path + end + + ## + # Return the Specification that listed the dependency + + def requester + @failed_dep.requester + end + +end + diff --git a/lib/rubygems/dependency_resolver/dependency_request.rb b/lib/rubygems/dependency_resolver/dependency_request.rb new file mode 100644 index 0000000000..05e447c3be --- /dev/null +++ b/lib/rubygems/dependency_resolver/dependency_request.rb @@ -0,0 +1,51 @@ +## +# Used Internally. Wraps a Dependency object to also track which spec +# contained the Dependency. + +class Gem::DependencyResolver::DependencyRequest + + attr_reader :dependency + + attr_reader :requester + + def initialize(dep, act) + @dependency = dep + @requester = act + end + + def ==(other) + case other + when Gem::Dependency + @dependency == other + when Gem::DependencyResolver::DependencyRequest + @dependency == other.dependency && @requester == other.requester + else + false + end + end + + def matches_spec?(spec) + @dependency.matches_spec? spec + end + + def name + @dependency.name + end + + def pretty_print q # :nodoc: + q.group 2, '[Dependency request ', ']' do + q.breakable + q.text @dependency.to_s + + q.breakable + q.text ' requested by ' + q.pp @requester + end + end + + def to_s # :nodoc: + @dependency.to_s + end + +end + diff --git a/lib/rubygems/dependency_resolver/index_set.rb b/lib/rubygems/dependency_resolver/index_set.rb new file mode 100644 index 0000000000..fcf919d81b --- /dev/null +++ b/lib/rubygems/dependency_resolver/index_set.rb @@ -0,0 +1,59 @@ +## +# The global rubygems pool represented via the traditional +# source index. + +class Gem::DependencyResolver::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 << Gem::DependencyResolver::IndexSpecification.new( + self, n.name, n.version, uri, n.platform) + end + end + + res + 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 + + ## + # No prefetching needed since we load the whole index in + # initially. + + def prefetch gems + end + +end + diff --git a/lib/rubygems/dependency_resolver/index_specification.rb b/lib/rubygems/dependency_resolver/index_specification.rb new file mode 100644 index 0000000000..371018ba44 --- /dev/null +++ b/lib/rubygems/dependency_resolver/index_specification.rb @@ -0,0 +1,53 @@ +## +# 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 Gem::DependencyResolver::IndexSpecification + + attr_reader :name + + attr_reader :source + + attr_reader :version + + def initialize set, name, version, source, plat + @set = set + @name = name + @version = version + @source = source + @platform = plat + + @spec = nil + end + + def dependencies + spec.dependencies + end + + def full_name + "#{@name}-#{@version}" + end + + def inspect # :nodoc: + '#<%s %s source %s>' % [self.class, full_name, @source] + end + + def pretty_print q # :nodoc: + q.group 2, '[Index specification', ']' do + q.breakable + q.text full_name + + q.breakable + q.text ' source ' + q.pp @source + end + end + + def spec + @spec ||= @set.load_spec(@name, @version, @source) + end + +end + diff --git a/lib/rubygems/dependency_resolver/installed_specification.rb b/lib/rubygems/dependency_resolver/installed_specification.rb new file mode 100644 index 0000000000..af167572bf --- /dev/null +++ b/lib/rubygems/dependency_resolver/installed_specification.rb @@ -0,0 +1,38 @@ +class Gem::DependencyResolver::InstalledSpecification + + attr_reader :spec + + def initialize set, spec, source=nil + @set = set + @source = source + @spec = spec + end + + def == other # :nodoc: + self.class === other and + @set == other.set and + @spec == other.spec + end + + def dependencies + @spec.dependencies + end + + def full_name + "#{@spec.name}-#{@spec.version}" + end + + def name + @spec.name + end + + def source + @source ||= Gem::Source::Installed.new + end + + def version + @spec.version + end + +end + diff --git a/lib/rubygems/dependency_resolver/installer_set.rb b/lib/rubygems/dependency_resolver/installer_set.rb new file mode 100644 index 0000000000..7de052df77 --- /dev/null +++ b/lib/rubygems/dependency_resolver/installer_set.rb @@ -0,0 +1,130 @@ +class Gem::DependencyResolver::InstallerSet + + ## + # List of Gem::Specification objects that must always be installed. + + attr_reader :always_install + + ## + # Only install gems in the always_install list + + attr_accessor :ignore_dependencies + + ## + # Do not look in the installed set when finding specifications. This is + # used by the --install-dir option to `gem install` + + attr_accessor :ignore_installed + + def initialize domain + @domain = domain + + @f = Gem::SpecFetcher.fetcher + + @all = Hash.new { |h,k| h[k] = [] } + @always_install = [] + @ignore_dependencies = false + @ignore_installed = false + @loaded_remote_specs = [] + @specs = {} + end + + ## + # Should local gems should be considered? + + def consider_local? + @domain == :both or @domain == :local + end + + ## + # Should remote gems should be considered? + + def consider_remote? + @domain == :both or @domain == :remote + end + + ## + # Returns an array of IndexSpecification objects matching DependencyRequest + # +req+. + + def find_all req + res = [] + + dep = req.dependency + + return res if @ignore_dependencies and + @always_install.none? { |spec| dep.matches_spec? spec } + + name = dep.name + + dep.matching_specs.each do |gemspec| + next if @always_install.include? gemspec + + res << Gem::DependencyResolver::InstalledSpecification.new(self, gemspec) + end unless @ignore_installed + + if consider_local? then + local_source = Gem::Source::Local.new + + if spec = local_source.find_gem(name, dep.requirement) then + res << Gem::DependencyResolver::IndexSpecification.new( + self, spec.name, spec.version, local_source, spec.platform) + end + end + + if consider_remote? then + load_remote_specs dep + + @all[name].each do |remote_source, n| + if dep.match? n then + res << Gem::DependencyResolver::IndexSpecification.new( + self, n.name, n.version, remote_source, n.platform) + end + end + end + + res + end + + def inspect # :nodoc: + '#<%s domain: %s specs: %p>' % [ self.class, @domain, @specs.keys ] + end + + ## + # Loads remote prerelease specs if +dep+ is a prerelease dependency + + def load_remote_specs dep + types = [:released] + types << :prerelease if dep.prerelease? + + types.each do |type| + next if @loaded_remote_specs.include? type + @loaded_remote_specs << type + + list, = @f.available_specs type + + list.each do |uri, specs| + specs.each do |n| + @all[n.name] << [uri, n] + end + end + end + 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 + + ## + # No prefetching needed since we load the whole index in initially. + + def prefetch(reqs) + end + +end + diff --git a/lib/rubygems/exceptions.rb b/lib/rubygems/exceptions.rb index ff389b320b..13cb5c7f6e 100644 --- a/lib/rubygems/exceptions.rb +++ b/lib/rubygems/exceptions.rb @@ -16,6 +16,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 +87,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 +165,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 ab454b4ba1..74689bb5e2 100644 --- a/lib/rubygems/ext/builder.rb +++ b/lib/rubygems/ext/builder.rb @@ -18,7 +18,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 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..6446cc9799 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,6 +36,47 @@ module Gem::GemcutterUtilities end end + ## + # The host to connect to either from the RUBYGEMS_HOST environment variable + # or from the user's configuration + + def host + configured_host = Gem.host unless + Gem.configuration.disable_default_gem_server + + @host ||= + begin + env_rubygems_host = ENV['RUBYGEMS_HOST'] + env_rubygems_host = nil if + env_rubygems_host and env_rubygems_host.empty? + + env_rubygems_host|| configured_host + 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' + + self.host = host if host + unless self.host + alert_error "You must specify a gem server" + terminate_interaction 1 # TODO: question this + end + + uri = URI.parse "#{self.host}/#{path}" + + request_method = Net::HTTP.const_get method.to_s.capitalize + + Gem::RemoteFetcher.fetcher.request(uri, request_method, &block) + end + + ## + # Signs in with the RubyGems API at +sign_in_host+ and sets the rubygems API + # key. + def sign_in sign_in_host = self.host return if Gem.configuration.rubygems_api_key @@ -55,53 +105,9 @@ module Gem::GemcutterUtilities end end - attr_writer :host - def host - configured_host = Gem.host unless - Gem.configuration.disable_default_gem_server - - @host ||= - begin - env_rubygems_host = ENV['RUBYGEMS_HOST'] - env_rubygems_host = nil if - env_rubygems_host and env_rubygems_host.empty? - - env_rubygems_host|| configured_host - end - end - - def rubygems_api_request(method, path, host = nil, &block) - require 'net/http' - - self.host = host if host - unless self.host - alert_error "You must specify a gem server" - terminate_interaction 1 # TODO: question this - end - - uri = URI.parse "#{self.host}/#{path}" - - request_method = Net::HTTP.const_get method.to_s.capitalize - - 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 - - say message - terminate_interaction 1 # TODO: question this - 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 @@ -112,4 +118,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_default_message.rb b/lib/rubygems/install_default_message.rb new file mode 100644 index 0000000000..458ba3da96 --- /dev/null +++ b/lib/rubygems/install_default_message.rb @@ -0,0 +1,12 @@ +require 'rubygems' +require 'rubygems/user_interaction' + +## +# A post-install hook that displays "Successfully installed +# some_gem-1.0 as a default gem" + +Gem.post_install do |installer| + ui = Gem::DefaultUserInteraction.ui + ui.say "Successfully installed #{installer.spec.full_name} as a default gem" +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 939bc9693d..c6fad3c492 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -212,16 +212,21 @@ class Gem::Installer FileUtils.rm_rf gem_dir FileUtils.mkdir_p gem_dir - - extract_files - - build_extensions - write_build_info_file - run_post_build_hooks - - generate_bin - write_spec - write_cache_file + + if @options[:install_as_default] + extract_bin + write_default_spec + else + extract_files + + build_extensions + write_build_info_file + run_post_build_hooks + + generate_bin + write_spec + write_cache_file + end say spec.post_install_message unless spec.post_install_message.nil? @@ -326,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. @@ -336,6 +349,16 @@ class Gem::Installer file.fsync rescue nil # for filesystems without fsync(2) 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 @@ -538,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] @@ -715,6 +738,15 @@ EOF def extract_files @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. @@ -756,7 +788,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..76f45a99d8 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.rb @@ -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,10 +332,15 @@ 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| + next unless File.fnmatch pattern, entry.full_name + destination = install_location entry.full_name, destination_dir FileUtils.rm_rf destination @@ -428,12 +441,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 +523,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..5a16d7a6df 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,10 @@ 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 end private diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb index 4a4674b72f..21345d73df 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 diff --git a/lib/rubygems/remote_fetcher.rb b/lib/rubygems/remote_fetcher.rb index ec052b50da..c10f9ebae8 100644 --- a/lib/rubygems/remote_fetcher.rb +++ b/lib/rubygems/remote_fetcher.rb @@ -1,6 +1,7 @@ require 'rubygems' +require 'rubygems/request' +require 'rubygems/uri_formatter' require 'rubygems/user_interaction' -require 'uri' require 'resolv' ## @@ -71,17 +72,7 @@ class Gem::RemoteFetcher Socket.do_not_reverse_lookup = true - @connections = {} - @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 @@ -200,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 @@ -319,125 +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 ':' - @connections[connection_id] ||= Net::HTTP.new(*net_http_args) - connection = @connections[connection_id] - - 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] @@ -446,136 +318,13 @@ 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 - - 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}" - end - ua << ")" - - ua << " #{RUBY_ENGINE}" if defined?(RUBY_ENGINE) and RUBY_ENGINE != 'ruby' - - ua + Gem::Request.new(uri, request_class, last_modified, @proxy).fetch end def https?(uri) diff --git a/lib/rubygems/request.rb b/lib/rubygems/request.rb new file mode 100644 index 0000000000..60e31838f9 --- /dev/null +++ b/lib/rubygems/request.rb @@ -0,0 +1,262 @@ +require 'net/http' +require 'time' +require 'rubygems/user_interaction' + +class Gem::Request + + include Gem::UserInteraction + + attr_reader :proxy_uri + + def initialize(uri, request_class, last_modified, proxy) + @uri = uri + @request_class = request_class + @last_modified = last_modified + @requests = Hash.new 0 + @connections = {} + @user_agent = user_agent + + @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 + @env_no_proxy = get_no_proxy_from_env + 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 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_client_cert then + pem = File.read Gem.configuration.ssl_client_cert + connection.cert = OpenSSL::X509::Certificate.new pem + connection.key = OpenSSL::PKey::RSA.new pem + end + + 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 + + ## + # 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 ':' + @connections[connection_id] ||= Net::HTTP.new(*net_http_args) + connection = @connections[connection_id] + + 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 Gem::RemoteFetcher::FetchError.new(e.message, uri) + end + + def fetch + 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 Gem::RemoteFetcher::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 Gem::RemoteFetcher::FetchError.new('too many connection resets', @uri) if retried + + reset connection + + retried = true + retry + end + + response + 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(Gem::UriFormatter.new(env_proxy).normalize) + + if uri and uri.user.nil? and uri.password.nil? then + # Probably we have http_proxy_* variables? + uri.user = Gem::UriFormatter.new(ENV['http_proxy_user'] || ENV['HTTP_PROXY_USER']).escape + uri.password = Gem::UriFormatter.new(ENV['http_proxy_pass'] || ENV['HTTP_PROXY_PASS']).escape + end + + uri + end + + def https?(uri) + uri.scheme.downcase == 'https' + 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 + + ## + # 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}" + end + ua << ")" + + ua << " #{RUBY_ENGINE}" if defined?(RUBY_ENGINE) and RUBY_ENGINE != 'ruby' + + ua + end + +end + diff --git a/lib/rubygems/request_set.rb b/lib/rubygems/request_set.rb index 6c52b90c40..748c320c28 100644 --- a/lib/rubygems/request_set.rb +++ b/lib/rubygems/request_set.rb @@ -5,178 +5,176 @@ 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? + attr_reader :always_install + + attr_reader :dependencies + + attr_accessor :development + + ## + # Treat missing dependencies as silent errors + + attr_accessor :soft_missing + + def initialize *deps + @dependencies = deps + + @always_install = [] + @development = false + @soft_missing = false + + yield self if block_given? + end + + ## + # Declare that a gem of name +name+ with +reqs+ requirements is needed. + + def gem name, *reqs + @dependencies << Gem::Dependency.new(name, reqs) + end + + ## + # Add +deps+ Gem::Dependency objects to the set. + + def import deps + @dependencies += deps + end + + def install options, &block + if dir = options[:install_dir] + return install_into dir, false, options, &block end - attr_reader :dependencies + cache_dir = options[:cache_dir] || Gem.dir - # Declare that a gem of name +name+ with +reqs+ requirements - # is needed. - # - def gem(name, *reqs) - @dependencies << Gem::Dependency.new(name, reqs) + 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 + + path = req.download cache_dir + + inst = Gem::Installer.new path, options + + yield req, inst if block_given? + + specs << inst.install end - # Add +deps+ Gem::Depedency objects to the set. - # - def import(deps) - @dependencies += deps + specs + end + + def install_into dir, force = true, options = {} + existing = force ? [] : specs_in(dir) + existing.delete_if { |s| @always_install.include? s } + + dir = File.expand_path dir + + installed = [] + + sorted_requests.each do |req| + if existing.find { |s| s.full_name == req.spec.full_name } + yield req, nil if block_given? + next + end + + path = req.download(dir) + + unless path then # already installed + yield req, nil if block_given? + next + end + + options[:install_dir] = dir + options[:only_install_dir] = true + + inst = Gem::Installer.new path, options + + yield req, inst if block_given? + + inst.install + + installed << req end - # Resolve the requested dependencies and return an Array of - # Specification objects to be activated. - # - def resolve(set=nil) - r = Gem::DependencyResolver.new(@dependencies, set) - @requests = r.resolve + installed + end + + ## + # Load a dependency management file. + + def load_gemdeps path + gf = Gem::RequestSet::GemDepedencyAPI.new self, path + gf.load + end + + ## + # Resolve the requested dependencies and return an Array of Specification + # objects to be activated. + + def resolve set = nil + resolver = Gem::DependencyResolver.new @dependencies, set + resolver.development = @development + resolver.soft_missing = @soft_missing + + @requests = resolver.resolve + end + + ## + # Resolve the requested dependencies against the gems available via Gem.path + # and return an Array of Specification objects to be activated. + + def resolve_current + resolve Gem::DependencyResolver::CurrentSet.new + end + + def sorted_requests + @sorted ||= strongly_connected_components.flatten + 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 - # 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 tsort_each_node &block # :nodoc: + @requests.each(&block) + end - # Load a dependency management file. - # - def load_gemdeps(path) - gf = GemDepedencyAPI.new(self, path) - gf.load - end + def tsort_each_child node # :nodoc: + node.spec.dependencies.each do |dep| + next if dep.type == :development and not @development - def specs - @specs ||= @requests.map { |r| r.full_spec } - end - - def tsort_each_node(&block) - @requests.each(&block) - end - - def tsort_each_child(node) - node.spec.dependencies.each do |dep| - next if dep.type == :development - - match = @requests.find { |r| dep.match? r.spec.name, r.spec.version } - if match - begin - yield match - rescue TSort::Cyclic - end - else + 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 - - def sorted_requests - @sorted ||= strongly_connected_components.flatten - end - - def specs_in(dir) - Dir["#{dir}/specifications/*.gemspec"].map do |g| - Gem::Specification.load g - end - end - - def install_into(dir, force=true, &b) - existing = force ? [] : specs_in(dir) - - dir = File.expand_path dir - - installed = [] - - sorted_requests.each do |req| - if existing.find { |s| s.full_name == req.spec.full_name } - b.call req, nil if b - next - end - - path = req.download(dir) - - inst = Gem::Installer.new path, :install_dir => dir, - :only_install_dir => true - - b.call req, inst if b - - inst.install - - installed << req - end - - installed - end - - def install(options, &b) - if dir = options[:install_dir] - return install_into(dir, false, &b) - end - - cache_dir = options[:cache_dir] || Gem.dir - - specs = [] - - sorted_requests.each do |req| - if req.installed? - b.call req, nil if b - next - end - - path = req.download cache_dir - - inst = Gem::Installer.new path, options - - b.call req, inst if b - - specs << inst.install - end - - specs - end - - # A semi-compatible DSL for Bundler's Gemfile format - # - class GemDepedencyAPI - def initialize(set, path) - @set = set - @path = path - end - - def load - instance_eval File.read(@path).untaint, @path, 1 - end - - # DSL - - def source(url) - end - - def gem(name, *reqs) - # Ignore the opts for now. - reqs.pop if reqs.last.kind_of?(Hash) - - @set.gem name, *reqs - end - - def platform(what) - if what == :ruby - yield - end - end - - alias_method :platforms, :platform - - def group(*what) - end - end end + end + +require 'rubygems/request_set/gem_dependency_api' diff --git a/lib/rubygems/request_set/gem_dependency_api.rb b/lib/rubygems/request_set/gem_dependency_api.rb new file mode 100644 index 0000000000..178ed1b59d --- /dev/null +++ b/lib/rubygems/request_set/gem_dependency_api.rb @@ -0,0 +1,39 @@ +## +# A semi-compatible DSL for Bundler's Gemfile format + +class Gem::RequestSet::GemDepedencyAPI + + def initialize set, path + @set = set + @path = path + end + + def load + instance_eval File.read(@path).untaint, @path, 1 + end + + # :category: Bundler Gemfile DSL + + def gem name, *reqs + # Ignore the opts for now. + reqs.pop if reqs.last.kind_of?(Hash) + + @set.gem name, *reqs + end + + def group *what + end + + def platform what + if what == :ruby + yield + end + end + + alias :platforms :platform + + def source url + end + +end + diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb index bed47ab9f3..ef0ff12cda 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('aes256') 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' -require 'rubygems/security/signer' -require 'rubygems/security/trust_dir' +if defined?(OpenSSL::SSL) then + require 'rubygems/security/policy' + require 'rubygems/security/policies' + require 'rubygems/security/trust_dir' +end + +require 'rubygems/security/signer' diff --git a/lib/rubygems/security/policy.rb b/lib/rubygems/security/policy.rb index 467ee932b5..98e41b812c 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,21 @@ 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" + else + alert_warning "#{full_name} is not signed" + end end opt = @opt @@ -222,7 +244,11 @@ 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 + else + alert_warning "#{subject signer} is not trusted for #{full_name}" + end signatures.each do |file, _| digest = signer_digests[file] @@ -252,7 +278,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..231f2fe604 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 diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb index 96d57870e2..8322ac33d4 100644 --- a/lib/rubygems/source.rb +++ b/lib/rubygems/source.rb @@ -25,14 +25,21 @@ class Gem::Source end def <=>(other) - if !@uri - return 0 unless other.uri - return -1 + case other + when Gem::Source::Installed, Gem::Source::Local then + -1 + when Gem::Source then + if !@uri + return 0 unless other.uri + return -1 + end + + return 1 if !other.uri + + @uri.to_s <=> other.uri.to_s + else + nil end - - return 1 if !other.uri - - @uri.to_s <=> other.uri.to_s end include Comparable @@ -58,8 +65,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 +147,14 @@ 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' diff --git a/lib/rubygems/source/installed.rb b/lib/rubygems/source/installed.rb new file mode 100644 index 0000000000..7709778791 --- /dev/null +++ b/lib/rubygems/source/installed.rb @@ -0,0 +1,28 @@ +class Gem::Source::Installed < Gem::Source + + def initialize + end + + ## + # Installed sources sort before all other sources + + def <=> other + case other + when Gem::Source::Installed then + 0 + when Gem::Source then + 1 + else + nil + end + end + + ## + # We don't need to download an installed gem + + def download spec, path + nil + end + +end + diff --git a/lib/rubygems/source/local.rb b/lib/rubygems/source/local.rb new file mode 100644 index 0000000000..7392ff0e8e --- /dev/null +++ b/lib/rubygems/source/local.rb @@ -0,0 +1,122 @@ +require 'rubygems/source' + +class Gem::Source::Local < Gem::Source + def initialize + @uri = nil + end + + ## + # Local sorts before Gem::Source and after Gem::Source::Installed + + def <=> other + case other + when Gem::Source::Installed then + -1 + when Gem::Source::Local then + 0 + when Gem::Source then + 1 + else + nil + end + end + + def inspect # :nodoc: + "#<%s specs: %p>" % [self.class, @specs.keys] + end + + def load_specs(type) + names = [] + + @specs = {} + + Dir["*.gem"].each do |file| + begin + pkg = Gem::Package.new(file) + rescue SystemCallError, Gem::Package::FormatError + # ignore + else + tup = pkg.spec.name_tuple + @specs[tup] = [File.expand_path(file), pkg] + + case type + when :released + unless pkg.spec.version.prerelease? + names << pkg.spec.name_tuple + end + when :prerelease + if pkg.spec.version.prerelease? + names << pkg.spec.name_tuple + end + when :latest + tup = pkg.spec.name_tuple + + cur = names.find { |x| x.name == tup.name } + if !cur + names << tup + elsif cur.version < tup.version + names.delete cur + names << tup + end + else + names << pkg.spec.name_tuple + end + end + end + + names + end + + def find_gem(gem_name, version=Gem::Requirement.default, + prerelease=false) + load_specs :complete + + found = [] + + @specs.each do |n, data| + if n.name == gem_name + s = data[1].spec + + if version.satisfied_by?(s.version) + if prerelease + found << s + elsif !s.version.prerelease? + found << s + end + end + end + end + + found.sort_by { |s| s.version }.last + end + + def fetch_spec(name) + load_specs :complete + + if data = @specs[name] + data.last.spec + else + raise Gem::Exception, "Unable to find spec for '#{name}'" + end + end + + def download(spec, cache_dir=nil) + load_specs :complete + + @specs.each do |name, data| + return data[0] if data[1].spec == spec + end + + raise Gem::Exception, "Unable to find file for '#{spec.full_name}'" + end + + def pretty_print q # :nodoc: + q.group 2, '[Local gems:', ']' do + q.breakable + q.seplist @specs.keys do |v| + q.text v.full_name + end + end + end + +end diff --git a/lib/rubygems/source/specific_file.rb b/lib/rubygems/source/specific_file.rb new file mode 100644 index 0000000000..d296e617cc --- /dev/null +++ b/lib/rubygems/source/specific_file.rb @@ -0,0 +1,28 @@ +class Gem::Source::SpecificFile < Gem::Source + def initialize(file) + @uri = nil + @path = ::File.expand_path(file) + + @package = Gem::Package.new @path + @spec = @package.spec + @name = @spec.name_tuple + end + + attr_reader :spec + + def load_specs(*a) + [@name] + end + + def fetch_spec(name) + return @spec if name == @name + raise Gem::Exception, "Unable to find '#{name}'" + @spec + end + + def download(spec, dir=nil) + return @path if spec == @spec + raise Gem::Exception, "Unable to download '#{spec.full_name}'" + end + +end diff --git a/lib/rubygems/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..967ab4492a 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 = {} diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb index 08614cd9eb..596f6ff69f 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 @@ -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| @@ -583,11 +601,6 @@ 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. @@ -615,44 +628,9 @@ class Gem::Specification attr_accessor :specification_version - class << self - def default_specifications_dir - File.join(Gem.default_dir, "specifications", "default") - 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 - } - } - end - - def each_default(&block) # :nodoc: - each_spec([default_specifications_dir], - &block) - end - - def each_normal(&block) # :nodoc: - each_spec(dirs, &block) - end - end - def self._all # :nodoc: unless defined?(@@all) && @@all then - - specs = {} - each_default do |spec| - specs[spec.full_name] ||= spec - end - each_normal do |spec| - specs[spec.full_name] ||= spec - end - - @@all = specs.values + @@all = stubs.map(&:to_spec) # After a reset, make sure already loaded specs # are still marked as activated. @@ -660,13 +638,58 @@ class Gem::Specification Gem.loaded_specs.each_value{|s| specs[s] = true} @@all.each{|s| s.activated = true if specs[s]} - _resort! + _resort!(@@all) end @@all end - def self._resort! # :nodoc: - @@all.sort! { |a, b| + 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 + + # :nodoc: + def self.each_stub(dirs) + each_gemspec(dirs) do |path| + stub = Gem::StubSpecification.new(path) + yield stub if stub.valid? + end + end + + # :nodoc: + def self.each_spec(dirs) + each_gemspec(dirs) do |path| + spec = self.load path + yield spec if spec + end + end + + ## + # Returns a Gem::StubSpecification for every installed gem + + def self.stubs + @@stubs ||= begin + stubs = {} + each_stub([default_specifications_dir] + dirs) do |stub| + stubs[stub.full_name] ||= stub + end + + stubs = stubs.values + _resort!(stubs) + stubs + end + end + + def self._resort!(specs) # :nodoc: + specs.sort! { |a, b| names = a.name <=> b.name next names if names.nonzero? b.version <=> a.version @@ -677,7 +700,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 @@ -700,7 +725,9 @@ class Gem::Specification return if _all.include? spec _all << spec - _resort! + stubs << spec + _resort!(_all) + _resort!(stubs) end ## @@ -843,9 +870,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 ## @@ -937,6 +965,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 @@ -950,6 +981,7 @@ class Gem::Specification if Gem::Specification === spec spec.loaded_from = file.to_s + LOAD_CACHE[file] = spec return spec end @@ -1013,6 +1045,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 ## @@ -1037,6 +1070,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" @@ -1280,20 +1315,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. # @@ -1367,19 +1388,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 TODAY. @@ -1623,35 +1631,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 ## @@ -1662,15 +1649,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. # @@ -1703,9 +1681,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: @@ -1720,7 +1696,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| @@ -1729,11 +1705,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 @@ -1828,27 +1800,30 @@ class Gem::Specification @licenses ||= [] end - ## - # Set the location a Specification was loaded from. +obj+ is converted - # to a String. + def filename= path + super - def loaded_from= path - @loaded_from = path.to_s - - # reset everything @loaded_from depends upon - @base_dir = nil @bin_dir = nil @cache_dir = nil @cache_file = nil @doc_dir = nil - @full_gem_path = nil @gem_dir = nil - @gems_dir = nil @ri_dir = nil @spec_dir = nil @spec_file = nil end + ## + # Path this gemspec was loaded from. This attribute is not persisted. + + alias loaded_from filename + + ## + # Set the location a Specification was loaded from. +obj+ is converted + # to a String. + + alias loaded_from= filename= + ## # Sets the rubygems_version to the current RubyGems version. @@ -1878,6 +1853,11 @@ class Gem::Specification end end + # Prevent ruby hitting spec.method_missing when [[spec]].flatten is called + def to_ary # :nodoc: + nil + end + ## # Normalize the list of files so that: # * All file lists have redundancies removed. @@ -2093,6 +2073,13 @@ class Gem::Specification [@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 @@ -2172,6 +2159,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|" @@ -2259,6 +2247,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 @@ -2559,11 +2554,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/stub_specification.rb b/lib/rubygems/stub_specification.rb new file mode 100644 index 0000000000..0a6e70bb15 --- /dev/null +++ b/lib/rubygems/stub_specification.rb @@ -0,0 +1,112 @@ +module Gem + # Gem::StubSpecification reads the stub: line from the gemspec + # This prevents us having to eval the entire gemspec in order to + # find out certain information. + class StubSpecification < BasicSpecification + # :nodoc: + PREFIX = "# stub: " + + # :nodoc: + class StubLine + attr_reader :parts + + def initialize(data) + @parts = data[PREFIX.length..-1].split(" ") + end + + def name + @parts[0] + end + + def version + Gem::Version.new @parts[1] + end + + def platform + Gem::Platform.new @parts[2] + end + + def require_paths + @parts[3..-1].join(" ").split("\0") + end + end + + def initialize(filename) + self.filename = filename + @data = nil + @spec = nil + end + + ## + # Name of the gem + + def name + @name ||= data.name + end + + ## + # Version of the gem + + def version + @version ||= data.version + end + + ## + # Platform of the gem + + def platform + @platform ||= data.platform + end + + ## + # Require paths of the gem + + def require_paths + @require_paths ||= data.require_paths + end + + ## + # The full Gem::Specification for this gem, loaded from evalling its gemspec + + def to_spec + @spec ||= Gem::Specification.load(filename) + end + + ## + # True when this gem has been activated + + def activated? + loaded = Gem.loaded_specs[name] + loaded && loaded.version == version + end + + ## + # Is this StubSpecification valid? i.e. have we found a stub line, OR does + # the filename contain a valid gemspec? + + def valid? + data + end + + private + + ## + # If the gemspec contains a stubline, returns a StubLine instance. Otherwise + # returns the full Gem::Specification. + + def data + unless @data + File.open(filename, "r:UTF-8:-") do |file| + begin + file.readline # discard encoding line + stubline = file.readline.chomp + @data = StubLine.new(stubline) if stubline.start_with?(PREFIX) + rescue EOFError + end + end + end + + @data ||= to_spec + end + end +end diff --git a/lib/rubygems/test_case.rb b/lib/rubygems/test_case.rb index e92e5d868a..df5908ea2f 100644 --- a/lib/rubygems/test_case.rb +++ b/lib/rubygems/test_case.rb @@ -78,6 +78,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" } @@ -200,6 +217,7 @@ 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 @@ -221,6 +239,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 @@ -266,39 +287,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 ## @@ -332,6 +320,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 ## @@ -560,6 +589,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. @@ -1005,6 +1049,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. @@ -1074,18 +1136,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__ @@ -1094,17 +1156,24 @@ Also, a list: # :stopdoc: # only available in RubyGems tests - begin - PRIVATE_KEY = load_key 'private' - PRIVATE_KEY_PATH = key_path 'private' - PUBLIC_KEY = PRIVATE_KEY.public_key + PRIVATE_KEY_PASSPHRASE = 'Foo bar' - PUBLIC_CERT = load_cert 'public' - PUBLIC_CERT_PATH = cert_path 'public' + begin + PRIVATE_KEY = load_key 'private' + PRIVATE_KEY_PATH = key_path 'private' + + # 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/uri_formatter.rb b/lib/rubygems/uri_formatter.rb new file mode 100644 index 0000000000..deac3bfe75 --- /dev/null +++ b/lib/rubygems/uri_formatter.rb @@ -0,0 +1,39 @@ +require 'uri' + +class Gem::UriFormatter + attr_reader :uri + + def initialize uri + @uri = uri + end + + def escape + return unless @uri + escaper.escape @uri + end + + ## + # Normalize the URI by adding "http://" if it is missing. + + def normalize + (@uri =~ /^(https?|ftp|file):/i) ? @uri : "http://#{@uri}" + end + + def unescape + return unless @uri + escaper.unescape @uri + end + + private + + def escaper + @uri_parser ||= + begin + URI::Parser.new + rescue NameError + URI + end + end + +end + diff --git a/lib/rubygems/util/list.rb b/lib/rubygems/util/list.rb new file mode 100644 index 0000000000..9f540adcc1 --- /dev/null +++ b/lib/rubygems/util/list.rb @@ -0,0 +1,44 @@ +module Gem + List = Struct.new(:value, :tail) + + class List + def each + n = self + while n + yield n.value + n = n.tail + end + end + + def to_a + ary = [] + n = self + while n + ary.unshift n.value + n = n.tail + end + + ary + end + + def find + n = self + while n + v = n.value + return v if yield(v) + n = n.tail + end + + nil + end + + def prepend(value) + List.new value, self + end + + def self.prepend(list, value) + return List.new(value) unless list + List.new value, list + end + end +end diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb index e983751c17..fa9bbc5a9d 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 diff --git a/test/rubygems/ca_cert.pem b/test/rubygems/ca_cert.pem index 5acdcf8f32..5207531bc2 100644 --- a/test/rubygems/ca_cert.pem +++ b/test/rubygems/ca_cert.pem @@ -43,3 +43,26 @@ ySjIblqVQkPuzebv3Ror6ZnVDukn96Mg7kP4u6zgxOeqlJGRe1M949SS9Vudjl8X SF4aZUUB9pQGhsqQJVqaz2OlhGOp9D0q54xko/rekjAIcuDjl1mdX4F2WRrzpUmZ uY/bPeOBYiVsOYVe -----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIJANz6ehBcVuuiMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTMwNTAxMTQ0NTQxWhcNMjMwMzEwMTQ0NTQxWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAzlpZwhEYoALOEKU4lmMw5l3YI/gadzDOoELtdcidvVvovKK8IIOTDwbA +3XcjwV0UPGEPOK4Uk1aD0EKkOQVg8ivSre2a3FFGffs2kXck+doJMzAA+pf8tvFk +QsETVOurOp74GN+er2xbbRSDVxQKq6d+QTe1E60btyXQS5M1Nt5SvLn8dazZJgvv +3yzJQ1IOQl+xeEO0WVVhPIx5Mx3VtjjcDyl8aewPkYkzia6UOrAyQZnl5sIzWGOb +kYKCNeKjTPepzlbMx0dN6jBupPYGNB+4FYY9GezInjGbRP5np5382wd3EWwsVzic +Nau8kXHTL2r7GzNvoy0p//iPCqx9FQIDAQABo4GnMIGkMB0GA1UdDgQWBBS7B027 +H/ZIkW3ngm1SrR0X/aTCwDB1BgNVHSMEbjBsgBS7B027H/ZIkW3ngm1SrR0X/aTC +wKFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV +BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJANz6ehBcVuuiMAwGA1UdEwQF +MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAC0glUrUiylTfuOWlwkQvi74oiYC5CzW +Jfusg6o/Gg1XEuJhaHiYMsK/do16gSc6Za3934rHQbYu3mesyFkCWF9kD4J6/hEO +OQL8xmmgN7wS6GXy6oIODpny0MgnFrV4gd1aEx69NIfL/wXaM8Gw2sj1TnuGLs8+ +HFmWLRRH3WSR7ZLnqYzPVJwhHu8vtZBL9HZk1J6xyq00Nwi2Cz5WdiHamgaza3TS +OgBdWwDeSClwhrTJni4d30dbq+eNMByIZ7QNGBQivpFzDxeNV/2UBrTU0CilKG5Q +j7ZwknfKeA4xUTd8TMK3vKab5JJCfjbXOTHZQsYUcEEGSjOMS8/YVQs= +-----END CERTIFICATE----- diff --git a/test/rubygems/client.pem b/test/rubygems/client.pem new file mode 100644 index 0000000000..63a52c574a --- /dev/null +++ b/test/rubygems/client.pem @@ -0,0 +1,49 @@ +-----BEGIN CERTIFICATE----- +MIIDgTCCAmmgAwIBAgICEAIwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQVUx +EzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMg +UHR5IEx0ZDAeFw0xMzA1MDExNTAxMzFaFw0yMzAzMTAxNTAxMzFaMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCw ++lcrpdWcloQCgAlxcM3GjvBxZ3yjzi6SgXKRBSi54i0J1LXxznWKcJ5P/O1+j+7i +LjHK+OWqsa0+EbKTwSu+0tx20h0z++YJF9GWEoCwT5aH1kor/0+EQLgYnxBaF8GC +2xAbkRkWmbSu2aLDIey3lg7lqAazYqdS2wH0UjSDjFKDLxz9LwpfFm0yGL3DgwLW ++dobYkgt1A6F/8Pz6D2FjwYKcM8JE6w7KJSJDUvXcv2E18wmhZ/qF/MtFAF4coB1 +f5ALnz8YqY6eyDF5aY/VfaHZvXdirLlMH6/miie9GBVMnJWF0ah5ssbsMvcpmnDJ +qkiYju2e1oLFEE7zztU/AgMBAAGjezB5MAkGA1UdEwQCMAAwLAYJYIZIAYb4QgEN +BB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBTcOELj +hSUdiLrdRF3CFZDZkWaGzDAfBgNVHSMEGDAWgBS7B027H/ZIkW3ngm1SrR0X/aTC +wDANBgkqhkiG9w0BAQUFAAOCAQEAlQMzHlnT6L1qqA4hL6tABPbiMsVwXyKCcfNB +zBn82Wkxgbg7Mp31fbR6/qvGeXOtaX6IdPdgtVf8nh1NURk0MmFBP+gfnwfNBD+m +Q1cldDt9kY2LGIrPii40xbugF1/xqEYcZMgXU08aEvQ2IHX46J8wZoqMa2KhrU8/ +mzY0F+UEFOGWtKDgUzz3dyBPsdzVrX+SXULwH0lqZX8Nsw5LyfrlVt3xQvS5Ogm4 +kYlt8kqhF8lUS3WTbuADrIs3NaDPRWSs1iLRRFgosgUtHN7tkrkrVaHeBo0KbAJG +mMqtxSY0XZI9WBxffP9UtoY3EiTWNVWLtuCN3OSvryP6NDe4BA== +-----END CERTIFICATE----- + +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAsPpXK6XVnJaEAoAJcXDNxo7wcWd8o84ukoFykQUoueItCdS1 +8c51inCeT/ztfo/u4i4xyvjlqrGtPhGyk8ErvtLcdtIdM/vmCRfRlhKAsE+Wh9ZK +K/9PhEC4GJ8QWhfBgtsQG5EZFpm0rtmiwyHst5YO5agGs2KnUtsB9FI0g4xSgy8c +/S8KXxZtMhi9w4MC1vnaG2JILdQOhf/D8+g9hY8GCnDPCROsOyiUiQ1L13L9hNfM +JoWf6hfzLRQBeHKAdX+QC58/GKmOnsgxeWmP1X2h2b13Yqy5TB+v5oonvRgVTJyV +hdGoebLG7DL3KZpwyapImI7tntaCxRBO887VPwIDAQABAoIBAFOpdG3gzlNg3/Ti +nBQxdEVqKwYhGs3A2UlOwl8F5lPBNPNRx9UQeYZBaMV9VrQezJnFpqpB8Sg5KCGQ +ci/hAJIL0kalW0LI0Nz5ko10H7u5U/rQ9W1JG0j041JYV32Pf14husKdXBPQA5co +sQW30tSSrmYogUpp15mWiJz8A3EvqiCTlQv5JwwMFGnjVl8+HNfuLghK/vqY/Eb9 +YmwTKxPFejqN7E0Mud2ylNiuPTSLwBy8UvV9uxOlDc6lMyZjVRO0woiEzrjw5dKF +yf5tUkICRcPkekcx+XtpGrCMlRLl770bZBZX+YNmbYXVWhFp09cNR+U0KZqPNcDp +jg73vXECgYEA3huOKzfHGt3qUdMlEHd1FvQhW9fYIrmUSnuVYQJOnY8lFfKfmrOH +gpwOIHDNiVHYlhAJaNocCLYx4hWHgZXarY7NKxmlY2+Vp8mcCIf2Cw3Kr/sFklUJ +KpiRxqEPGR7U4C/E31kkH/C+w7m9Zh3ndhltU2Pki9/Eq0lk8YClMMkCgYEAy/vU +jxzviIk8bll5uCIuXJyCfao7ywaZABbL6a20kdVGKrHj57O/OJ2WZVwBihhB7OS+ +QsKC/J8LrUJkobOFtQvQ8O23uep5rB6kqCkXsXCG4SCl2L5xZySBp/qhiqbuMwvp +EAWPSIA6UNoR0J2rDYVmq6jtY526wQf5ivE8IccCgYEAphfzJAyNH2FOZixArmS2 +shiUjasG3UjsRRrP5YClK5wtPpF2m2if8KMkyUux2HvVPLr3XmqkxjsBaLFy6QwY +QOvmL9H45Tg/sP7KaXLLIw8IQLu2OezPcwQvF1u//6gXxyLR1bhClIQjFBjlMuUv +/xgasl6kPZlz6Cd1jkgGwEkCgYAI1IT2EQWZfn9cM4leXDRvk+LeN8FQ35897r6z +Be78JSRdcsfv3ssXU1MQXjQ+2x/3dkt6LltnPidOP8KFcXUHSlSoKVI7vRe5SLZO +BUFeUAW2tygWwt+73Eu0jtfxXZqQISLcq7DxLYPYvifpRPoDotO3+J8WIdzUwFig +GCNHPwKBgHqXOyRef7ykVUCptMf61/BvNU8NP1f9PkKQBMYQZC39UwqEQ675QBUh +hSG9t/kyc44zUVmBeKIlWHVyLQ83Dv+ennz/D9t7tstet0VMKvALNdiVT0sjFKN7 +1VINygCeFkqrlTXlOwFcRSo1gHn3/JIrhSgRuYKHSf0GZOcN6d9l +-----END RSA PRIVATE KEY----- diff --git a/test/rubygems/encrypted_private_key.pem b/test/rubygems/encrypted_private_key.pem new file mode 100644 index 0000000000..6acfda6ce4 --- /dev/null +++ b/test/rubygems/encrypted_private_key.pem @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,1AC78C928C296A1D7C70D525B0F1051C + +QL7dLRBmNpbSYsq+4niIdtP9LpJYQxG9tXaTKjQfgkYtLbDzhQMhxpKcJwCTtZUK +kJWxt7AOq8JwvvH69kp8fEULR5IThSPyFTjnLxtg1ZpMJZfHyfjAtveBO+Z4pCWA +Z6xrLI7RoFEVuSEgAkNYlb2JY4Z26nfCakvciEpHOkeYEYsneBQkr7Zf/IcKuKwd +wjOMzuLwvF3+cYaxcoHViRsuwyI6YrToJvPtin0xJlJczWalVSQciwjuDDGfjzow +J2o1O5UZc+VnEItpIbLWriRQPGP5ezOiTUCCxN+v/lignaeykfk+apAiKliKl2w6 +eyxfBAIt8yE3RyhE3mX+AZN8sX+mfduEXCcAziZLSTYm3Lfq90eKGs+cUMFmwz1N +NvFVfIHpiRSzKlrJlvd38SRbSbQfvS2OEo+e0f4ZW7cKCXayczwF0gQQY9VZ23bn +Sk1CGuA2ugn+cd9T/yrSTtgz1EDpZxp7HYE242DiJb7wUY30nAqgYZ//ug6HGBJA +OYQldiinj6lWr0i/jEdKknUKIZTQQ+aH0c+hvbsagQRoVFZUCG6RFbKtWHRxL/a0 +teMT1SFeab6pulh3/VfdLzdBKVvHaY3bpujAmOg4lq0O2MQWMGvIPdso9iTBoAJm +TrLR/YO0RfvnfC0uM2YHXcLlhgsBUiGQUNnk6EZ5qK2aEiZuaCecpsCYEt2uhO9W +HF7CpAh3T1OUY33HEw/4KdvMG+5uwK+4D1JatKHsU0Umpp2+2C9T6W/iSLXndg0L +Xr8NFu9ziXdEe4tZy/9VDo4QOnqFhSBXxkimGrdnUrbTxH4nwUzmv4VRnbAXTEJM +XkVat7zZ1dvUf+iJXiRxjo6BbwXtL6+ZmL1aYbnbN8HrQdhuFN/QD/OzhYj7f9Yn +sTSQUleAK1+sppcTs6tiEdxWBgnKUeQNCXEBXG4twy7rd2ymamvunBTaoywebcaG +RTnK8eyOkoDeVEFZx+EI2TrG2PaA0Zuq+7IYqID+6/asa4K/3J/ChXqjIAgqUcML +56DlF5DCTvaRRUwftARaOqJZ+VxoW62i30nP/oD35xh++Esf8YgxhPeg1Gjzozx7 +ZC1GZ5f44EvDJyFlXUUNtNy3dC3cSdUUM6oYvDLrPI3wVEw3QgLUJ+Tc8lA5Gx7M +wW2i/Y6JqlVUabvkaKe4d+w8eo219Bnfo7D199TppbEXOob6AaC2CJranActTfrm +fFrWQKJrdWz1mWZT3efoBpxVAds8fYk2hNaXL6LQepOAF6ObbS4hHcRHbI7HIdVB +6GNUfVWlrISZ6thj84way/niR1ikXUFipN5gCRERc0+brXK4OCnksyLqYgvMI74Z +5lW8HfuX4FNp/Gd5uU+tbYnNy6nIqa8oZScLp0Kjg9tPKjjrDbZS2LJ8kxf7q9lb +YbxhzMy+uKwdmxIB4fKjWZTgPX4MwjA8FAaMncyvA64rxGnfyLExmOOZWSXqZQ8z +y+xoqA239Wob98mJn+oluneMKwSAM3ActGTmp5X5jHVk++yEcJN9uGYAa3UohKlm +/wgpQ79yfBywju2rZR0hQXN0ExBdE/UnJucJMv/iB5fxlkJlkNJPwFgq8iMbzQLu +-----END RSA PRIVATE KEY----- diff --git a/test/rubygems/invalid_client.pem b/test/rubygems/invalid_client.pem new file mode 100644 index 0000000000..e0d39519d9 --- /dev/null +++ b/test/rubygems/invalid_client.pem @@ -0,0 +1,49 @@ +-----BEGIN CERTIFICATE----- +MIIDgTCCAmmgAwIBAgICEAIwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQVUx +EzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMg +UHR5IEx0ZDAeFw0xMzA1MDExNTAxMzFaFw0yMzAzMTAxNTAxMzFaMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eXXXdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCw ++lcrpdWcloQCgAlxcM3GjvBxZ3yjzi6SgXKRBSi54i0J1LXxznWKcJ5P/O1+j+7i +LjHK+OWqsa0+EbKTwSu+0tx20h0z++YJF9GWEoCwT5aH1kor/0+EQLgYnxBaF8GC +2xAbkRkWmbSu2aLDIey3lg7lqAazYqdS2wH0UjSDjFKDLxz9LwpfFm0yGL3DgwLW ++dobYkgt1A6F/8Pz6D2FjwYKcM8JE6w7KJSJDUvXcv2E18wmhZ/qF/MtFAF4coB1 +f5ALnz8YqY6eyDF5aY/VfaHZvXdirLlMH6/miie9GBVMnJWF0ah5ssbsMvcpmnDJ +qkiYju2e1oLFEE7zztU/AgMBAAGjezB5MAkGA1UdEwQCMAAwLAYJYIZIAYb4QgEN +BB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBTcOELj +hSUdiLrdRF3CFZDZkWaGzDAfBgNVHSMEGDAWgBS7B027H/ZIkW3ngm1SrR0X/aTC +wDANBgkqhkiG9w0BAQUFAAOCAQEAlQMzHlnT6L1qqA4hL6tABPbiMsVwXyKCcfNB +zBn82Wkxgbg7Mp31fbR6/qvGeXOtaX6IdPdgtVf8nh1NURk0MmFBP+gfnwfNBD+m +Q1cldDt9kY2LGIrPii40xbugF1/xqEYcZMgXU08aEvQ2IHX46J8wZoqMa2KhrU8/ +mzY0F+UEFOGWtKDgUzz3dyBPsdzVrX+SXULwH0lqZX8Nsw5LyfrlVt3xQvS5Ogm4 +kYlt8kqhF8lUS3WTbuADrIs3NaDPRWSs1iLRRFgosgUtHN7tkrkrVaHeBo0KbAJG +mMqtxSY0XZI9WBxffP9UtoY3EiTWNVWLtuCN3OSvryP6NDe4BA== +-----END CERTIFICATE----- + +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAsPpXK6XVnJaEAoAJcXDNxo7wcWd8o84ukoFykQUoueItCdS1 +8c51inCeT/ztfo/u4i4xyvjlqrGtPhGyk8ErvtLcdtIdM/vmCRfRlhKAsE+Wh9ZK +K/9PhEC4GJ8QWhfBgtsQG5EZFpm0rtmiwyHst5YO5agGs2KnUtsB9FI0g4xSgy8c +/S8KXxZtMhi9w4MC1vnaG2JILdQOhf/D8+g9hY8GCnDPCROsOyiUiQ1L13L9hNfM +JoWf6hfzLRQBeHKAdX+QC58/GKmOnsgxeWmP1X2h2b13Yqy5TB+v5oonvRgVTJyV +hdGoebLG7DL3KZpwyapImI7tntaCxRBO887VPwIDAQABAoIBAFOpdG3gzlNg3/Ti +nBQxdEVqKwYhGs3A2UlOwl8F5lPBNPNRx9UQeYZBaMV9VrQezJnFpqpB8Sg5KCGQ +ci/hAJIL0kalW0LI0Nz5ko10H7u5U/rQ9W1JG0j041JYV32Pf14husKdXBPQA5co +sQW30tSSrmYogUpp15mWiJz8A3EvqiCTlQv5JwwMFGnjVl8+HNfuLghK/vqY/Eb9 +YmwTKxPFejqN7E0Mud2ylNiuPTSLwBy8UvV9uxOlDc6lMyZjVRO0woiEzrjw5dKF +yf5tUkICRcPkekcx+XtpGrCMlRLl770bZBZX+YNmbYXVWhFp09cNR+U0KZqPNcDp +jg73vXECgYEA3huOKzfHGt3qUdMlEHd1FvQhW9fYIrmUSnuVYQJOnY8lFfKfmrOH +gpwOIHDNiVHYlhAJaNocCLYx4hWHgZXarY7NKxmlY2+Vp8mcCIf2Cw3Kr/sFklUJ +KpiRxqEPGR7U4C/E31kkH/C+w7m9Zh3ndhltU2Pki9/Eq0lk8YClMMkCgYEAy/vU +jxzviIk8bll5uCIuXJyCfao7ywaZABbL6a20kdVGKrHj57O/OJ2WZVwBihhB7OS+ +QsKC/J8LrUJkobOFtQvQ8O23uep5rB6kqCkXsXCG4SCl2L5xZySBp/qhiqbuMwvp +EAWPSIA6UNoR0J2rDYVmq6jtY526wQf5ivE8IccCgYEAphfzJAyNH2FOZixArmS2 +shiUjasG3UjsRRrP5YClK5wtPpF2m2if8KMkyUux2HvVPLr3XmqkxjsBaLFy6QwY +QOvmL9H45Tg/sP7KaXLLIw8IQLu2OezPcwQvF1u//6gXxyLR1bhClIQjFBjlMuUv +/xgasl6kPZlz6Cd1jkgGwEkCgYAI1IT2EQWZfn9cM4leXDRvk+LeN8FQ35897r6z +Be78JSRdcsfv3ssXU1MQXjQ+2x/3dkt6LltnPidOP8KFcXUHSlSoKVI7vRe5SLZO +BUFeUAW2tygWwt+73Eu0jtfxXZqQISLcq7DxLYPYvifpRPoDotO3+J8WIdzUwFig +GCNHPwKBgHqXOyRef7ykVUCptMf61/BvNU8NP1f9PkKQBMYQZC39UwqEQ675QBUh +hSG9t/kyc44zUVmBeKIlWHVyLQ83Dv+ennz/D9t7tstet0VMKvALNdiVT0sjFKN7 +1VINygCeFkqrlTXlOwFcRSo1gHn3/JIrhSgRuYKHSf0GZOcN6d9l +-----END RSA PRIVATE KEY----- diff --git a/test/rubygems/specifications/bar-0.0.2.gemspec b/test/rubygems/specifications/bar-0.0.2.gemspec new file mode 100644 index 0000000000..ceefa4ed16 --- /dev/null +++ b/test/rubygems/specifications/bar-0.0.2.gemspec @@ -0,0 +1,9 @@ +# -*- encoding: utf-8 -*- + +Gem::Specification.new do |s| + s.name = "bar" + s.version = "0.0.2" + s.platform = "ruby" + s.require_paths = ["lib"] + s.summary = "A very bar gem" +end diff --git a/test/rubygems/specifications/foo-0.0.1.gemspec b/test/rubygems/specifications/foo-0.0.1.gemspec new file mode 100644 index 0000000000..7fbc56429f Binary files /dev/null and b/test/rubygems/specifications/foo-0.0.1.gemspec differ diff --git a/test/rubygems/test_gem.rb b/test/rubygems/test_gem.rb index fda83767f2..a4e5313c5d 100644 --- a/test/rubygems/test_gem.rb +++ b/test/rubygems/test_gem.rb @@ -15,6 +15,7 @@ class TestGem < Gem::TestCase def setup super + common_installer_setup ENV.delete 'RUBYGEMS_GEMDEPS' @additional = %w[a b].map { |d| File.join @tempdir, d } @@ -22,119 +23,6 @@ class TestGem < Gem::TestCase util_remove_interrupt_command end - 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 - - def test_self_activate - foo = util_spec 'foo', '1' - - assert_activate %w[foo-1], foo - 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 - - # TODO: move these to specification - def test_self_activate_via_require - a1 = new_spec "a", "1", "b" => "= 1" - b1 = new_spec "b", "1", nil, "lib/b/c.rb" - b2 = new_spec "b", "2", nil, "lib/b/c.rb" - - install_specs a1, b1, b2 - - a1.activate - save_loaded_features do - require "b/c" - end - - assert_equal %w(a-1 b-1), loaded_spec_names - end - - # TODO: move these to specification - def test_self_activate_deep_unambiguous - a1 = new_spec "a", "1", "b" => "= 1" - b1 = new_spec "b", "1", "c" => "= 1" - b2 = new_spec "b", "2", "c" => "= 2" - c1 = new_spec "c", "1" - c2 = new_spec "c", "2" - - install_specs a1, b1, b2, c1, c2 - - a1.activate - assert_equal %w(a-1 b-1 c-1), loaded_spec_names - end - - def save_loaded_features - old_loaded_features = $LOADED_FEATURES.dup - yield - ensure - $LOADED_FEATURES.replace old_loaded_features - end - - # TODO: move these to specification - def test_self_activate_ambiguous_direct - save_loaded_features do - a1 = new_spec "a", "1", "b" => "> 0" - b1 = new_spec("b", "1", { "c" => ">= 1" }, "lib/d.rb") - b2 = new_spec("b", "2", { "c" => ">= 2" }, "lib/d.rb") - c1 = new_spec "c", "1" - c2 = new_spec "c", "2" - - Gem::Specification.reset - install_specs a1, b1, b2, c1, c2 - - a1.activate - assert_equal %w(a-1), loaded_spec_names - assert_equal ["b (> 0)"], unresolved_names - - require "d" - - assert_equal %w(a-1 b-2 c-2), loaded_spec_names - assert_equal [], unresolved_names - end - end - - # TODO: move these to specification - def test_self_activate_ambiguous_indirect - save_loaded_features do - a1 = new_spec "a", "1", "b" => "> 0" - b1 = new_spec "b", "1", "c" => ">= 1" - b2 = new_spec "b", "2", "c" => ">= 2" - c1 = new_spec "c", "1", nil, "lib/d.rb" - c2 = new_spec "c", "2", nil, "lib/d.rb" - - install_specs a1, b1, b2, c1, c2 - - a1.activate - assert_equal %w(a-1), loaded_spec_names - assert_equal ["b (> 0)"], unresolved_names - - require "d" - - assert_equal %w(a-1 b-2 c-2), loaded_spec_names - assert_equal [], unresolved_names - end - end - def test_self_finish_resolve save_loaded_features do a1 = new_spec "a", "1", "b" => "> 0" @@ -157,36 +45,6 @@ class TestGem < Gem::TestCase end end - def test_self_activate_via_require_wtf - save_loaded_features do - a1 = new_spec "a", "1", "b" => "> 0", "d" => "> 0" # this - b1 = new_spec "b", "1", { "c" => ">= 1" }, "lib/b.rb" - b2 = new_spec "b", "2", { "c" => ">= 2" }, "lib/b.rb" # this - c1 = new_spec "c", "1" - c2 = new_spec "c", "2" # this - d1 = new_spec "d", "1", { "c" => "< 2" }, "lib/d.rb" - d2 = new_spec "d", "2", { "c" => "< 2" }, "lib/d.rb" # this - - install_specs a1, b1, b2, c1, c2, d1, d2 - - a1.activate - - assert_equal %w(a-1), loaded_spec_names - assert_equal ["b (> 0)", "d (> 0)"], unresolved_names - - require "b" - - e = assert_raises Gem::LoadError do - require "d" - end - - assert_equal "unable to find a version of 'd' to activate", e.message - - assert_equal %w(a-1 b-2 c-2), loaded_spec_names - assert_equal ["d (> 0)"], unresolved_names - end - end - def test_self_finish_resolve_wtf save_loaded_features do a1 = new_spec "a", "1", "b" => "> 0", "d" => "> 0" # this @@ -211,94 +69,6 @@ class TestGem < Gem::TestCase end end - # TODO: move these to specification - def test_self_activate_ambiguous_unrelated - save_loaded_features do - a1 = new_spec "a", "1", "b" => "> 0" - b1 = new_spec "b", "1", "c" => ">= 1" - b2 = new_spec "b", "2", "c" => ">= 2" - c1 = new_spec "c", "1" - c2 = new_spec "c", "2" - d1 = new_spec "d", "1", nil, "lib/d.rb" - - install_specs a1, b1, b2, c1, c2, d1 - - a1.activate - assert_equal %w(a-1), loaded_spec_names - assert_equal ["b (> 0)"], unresolved_names - - require "d" - - assert_equal %w(a-1 d-1), loaded_spec_names - assert_equal ["b (> 0)"], unresolved_names - end - end - - # TODO: move these to specification - def test_self_activate_ambiguous_indirect_conflict - save_loaded_features do - a1 = new_spec "a", "1", "b" => "> 0" - a2 = new_spec "a", "2", "b" => "> 0" - b1 = new_spec "b", "1", "c" => ">= 1" - b2 = new_spec "b", "2", "c" => ">= 2" - c1 = new_spec "c", "1", nil, "lib/d.rb" - c2 = new_spec("c", "2", { "a" => "1" }, "lib/d.rb") # conflicts with a-2 - - install_specs a1, a2, b1, b2, c1, c2 - - a2.activate - assert_equal %w(a-2), loaded_spec_names - assert_equal ["b (> 0)"], unresolved_names - - require "d" - - assert_equal %w(a-2 b-1 c-1), loaded_spec_names - assert_equal [], unresolved_names - end - end - - # TODO: move these to specification - def test_require_already_activated - save_loaded_features do - a1 = new_spec "a", "1", nil, "lib/d.rb" - - install_specs a1 # , a2, b1, b2, c1, c2 - - a1.activate - assert_equal %w(a-1), loaded_spec_names - assert_equal [], unresolved_names - - assert require "d" - - assert_equal %w(a-1), loaded_spec_names - assert_equal [], unresolved_names - end - end - - # TODO: move these to specification - def test_require_already_activated_indirect_conflict - save_loaded_features do - a1 = new_spec "a", "1", "b" => "> 0" - a2 = new_spec "a", "2", "b" => "> 0" - b1 = new_spec "b", "1", "c" => ">= 1" - b2 = new_spec "b", "2", "c" => ">= 2" - c1 = new_spec "c", "1", nil, "lib/d.rb" - c2 = new_spec("c", "2", { "a" => "1" }, "lib/d.rb") # conflicts with a-2 - - install_specs a1, a2, b1, b2, c1, c2 - - a1.activate - c1.activate - assert_equal %w(a-1 c-1), loaded_spec_names - assert_equal ["b (> 0)"], unresolved_names - - assert require "d" - - assert_equal %w(a-1 c-1), loaded_spec_names - assert_equal ["b (> 0)"], unresolved_names - end - end - def test_require_missing save_loaded_features do assert_raises ::LoadError do @@ -321,221 +91,6 @@ class TestGem < Gem::TestCase end end - # TODO: move these to specification - def test_self_activate_loaded - foo = util_spec 'foo', '1' - - assert foo.activate - refute foo.activate - end - - ## - # [A] depends on - # [B] >= 1.0 (satisfied by 2.0) - # [C] depends on nothing - - def test_self_activate_unrelated - a = util_spec 'a', '1.0', 'b' => '>= 1.0' - util_spec 'b', '1.0' - c = util_spec 'c', '1.0' - - assert_activate %w[b-1.0 c-1.0 a-1.0], a, c, "b" - end - - ## - # [A] depends on - # [B] >= 1.0 (satisfied by 2.0) - # [C] = 1.0 depends on - # [B] ~> 1.0 - # - # and should resolve using b-1.0 - # TODO: move these to specification - - def test_self_activate_over - a = util_spec 'a', '1.0', 'b' => '>= 1.0', 'c' => '= 1.0' - util_spec 'b', '1.0' - util_spec 'b', '1.1' - util_spec 'b', '2.0' - util_spec 'c', '1.0', 'b' => '~> 1.0' - - a.activate - - assert_equal %w[a-1.0 c-1.0], loaded_spec_names - assert_equal ["b (>= 1.0, ~> 1.0)"], unresolved_names - end - - ## - # [A] depends on - # [B] ~> 1.0 (satisfied by 1.1) - # [C] = 1.0 depends on - # [B] = 1.0 - # - # and should resolve using b-1.0 - # - # TODO: this is not under, but over... under would require depth - # first resolve through a dependency that is later pruned. - - def test_self_activate_under - a, _ = util_spec 'a', '1.0', 'b' => '~> 1.0', 'c' => '= 1.0' - util_spec 'b', '1.0' - util_spec 'b', '1.1' - c, _ = util_spec 'c', '1.0', 'b' => '= 1.0' - - assert_activate %w[b-1.0 c-1.0 a-1.0], a, c, "b" - end - - ## - # [A1] depends on - # [B] > 0 (satisfied by 2.0) - # [B1] depends on - # [C] > 0 (satisfied by 1.0) - # [B2] depends on nothing! - # [C1] depends on nothing - - def test_self_activate_dropped - a1, = util_spec 'a', '1', 'b' => nil - util_spec 'b', '1', 'c' => nil - util_spec 'b', '2' - util_spec 'c', '1' - - assert_activate %w[b-2 a-1], a1, "b" - end - - ## - # [A] depends on - # [B] >= 1.0 (satisfied by 1.1) depends on - # [Z] - # [C] >= 1.0 depends on - # [B] = 1.0 - # - # and should backtrack to resolve using b-1.0, pruning Z from the - # resolve. - - def test_self_activate_raggi_the_edgecase_generator - a, _ = util_spec 'a', '1.0', 'b' => '>= 1.0', 'c' => '>= 1.0' - util_spec 'b', '1.0' - util_spec 'b', '1.1', 'z' => '>= 1.0' - c, _ = util_spec 'c', '1.0', 'b' => '= 1.0' - - assert_activate %w[b-1.0 c-1.0 a-1.0], a, c, "b" - end - - def test_self_activate_conflict - util_spec 'b', '1.0' - util_spec 'b', '2.0' - - gem "b", "= 1.0" - - assert_raises Gem::LoadError do - gem "b", "= 2.0" - end - end - - ## - # [A] depends on - # [C] = 1.0 depends on - # [B] = 2.0 - # [B] ~> 1.0 (satisfied by 1.0) - - def test_self_activate_checks_dependencies - a, _ = util_spec 'a', '1.0' - a.add_dependency 'c', '= 1.0' - a.add_dependency 'b', '~> 1.0' - - util_spec 'b', '1.0' - util_spec 'b', '2.0' - c, _ = util_spec 'c', '1.0', 'b' => '= 2.0' - - e = assert_raises Gem::LoadError do - assert_activate nil, a, c, "b" - end - - expected = "can't satisfy 'b (~> 1.0)', already activated 'b-2.0'" - assert_equal expected, e.message - end - - ## - # [A] depends on - # [B] ~> 1.0 (satisfied by 1.0) - # [C] = 1.0 depends on - # [B] = 2.0 - - def test_self_activate_divergent - a, _ = util_spec 'a', '1.0', 'b' => '~> 1.0', 'c' => '= 1.0' - util_spec 'b', '1.0' - util_spec 'b', '2.0' - c, _ = util_spec 'c', '1.0', 'b' => '= 2.0' - - e = assert_raises Gem::LoadError do - assert_activate nil, a, c, "b" - end - - assert_match(/Unable to activate c-1.0,/, e.message) - assert_match(/because b-1.0 conflicts with b .= 2.0/, e.message) - end - - ## - # DOC - - def test_self_activate_platform_alternate - @x1_m = util_spec 'x', '1' do |s| - s.platform = Gem::Platform.new %w[cpu my_platform 1] - end - - @x1_o = util_spec 'x', '1' do |s| - s.platform = Gem::Platform.new %w[cpu other_platform 1] - end - - @w1 = util_spec 'w', '1', 'x' => nil - - util_set_arch 'cpu-my_platform1' - - assert_activate %w[x-1-cpu-my_platform-1 w-1], @w1, @x1_m - end - - ## - # DOC - - def test_self_activate_platform_bump - @y1 = util_spec 'y', '1' - - @y1_1_p = util_spec 'y', '1.1' do |s| - s.platform = Gem::Platform.new %w[cpu my_platform 1] - end - - @z1 = util_spec 'z', '1', 'y' => nil - - assert_activate %w[y-1 z-1], @z1, @y1 - end - - ## - # [C] depends on - # [A] = 1.a - # [B] = 1.0 depends on - # [A] >= 0 (satisfied by 1.a) - - def test_self_activate_prerelease - @c1_pre = util_spec 'c', '1.a', "a" => "1.a", "b" => "1" - @a1_pre = util_spec 'a', '1.a' - @b1 = util_spec 'b', '1' do |s| - s.add_dependency 'a' - s.add_development_dependency 'aa' - end - - assert_activate %w[a-1.a b-1 c-1.a], @c1_pre, @a1_pre, @b1 - end - - ## - # DOC - - def test_self_activate_old_required - e1, = util_spec 'e', '1', 'd' => '= 1' - @d1 = util_spec 'd', '1' - @d2 = util_spec 'd', '2' - - assert_activate %w[d-1 e-1], e1, "d" - end - def test_self_bin_path_no_exec_name e = assert_raises ArgumentError do Gem.bin_path 'a' @@ -1557,6 +1112,34 @@ class TestGem < Gem::TestCase assert_equal '["a-1", "b-1", "c-1"]', out.strip end + + def test_register_default_spec + Gem.clear_default_specs + + old_style = Gem::Specification.new do |spec| + spec.files = ["foo.rb", "bar.rb"] + end + + Gem.register_default_spec old_style + + assert_equal old_style, Gem.find_unresolved_default_spec("foo.rb") + assert_equal old_style, Gem.find_unresolved_default_spec("bar.rb") + assert_equal nil, Gem.find_unresolved_default_spec("baz.rb") + + Gem.clear_default_specs + + new_style = Gem::Specification.new do |spec| + spec.files = ["lib/foo.rb", "ext/bar.rb", "bin/exec", "README"] + spec.require_paths = ["lib", "ext"] + end + + Gem.register_default_spec new_style + + assert_equal new_style, Gem.find_unresolved_default_spec("foo.rb") + assert_equal new_style, Gem.find_unresolved_default_spec("bar.rb") + assert_equal nil, Gem.find_unresolved_default_spec("exec") + assert_equal nil, Gem.find_unresolved_default_spec("README") + end def with_plugin(path) test_plugin_path = File.expand_path("test/rubygems/plugin/#{path}", @@ -1634,4 +1217,3 @@ class TestGem < Gem::TestCase File.join Gem.dir, "cache" end end - diff --git a/test/rubygems/test_gem_commands_cert_command.rb b/test/rubygems/test_gem_commands_cert_command.rb index 75db6c4e15..a158a4442b 100644 --- a/test/rubygems/test_gem_commands_cert_command.rb +++ b/test/rubygems/test_gem_commands_cert_command.rb @@ -2,8 +2,8 @@ require 'rubygems/test_case' require 'rubygems/commands/cert_command' require 'rubygems/fix_openssl_warnings' if RUBY_VERSION < "1.9" -unless defined? OpenSSL then - warn "`gem cert` tests are being skipped, module OpenSSL not found" +unless defined?(OpenSSL::SSL) then + warn 'Skipping `gem cert` tests. openssl not found.' end class TestGemCommandsCertCommand < Gem::TestCase @@ -98,14 +98,22 @@ Added '/CN=alternate/DC=example' end def test_execute_build + passphrase = 'Foo bar' + @cmd.handle_options %W[--build nobody@example.com] - use_ui @ui do + @build_ui = Gem::MockGemUi.new "#{passphrase}\n#{passphrase}" + + use_ui @build_ui do @cmd.execute end - output = @ui.output.split "\n" + output = @build_ui.output.split "\n" + assert_equal "Passphrase for your Private Key: ", + output.shift + assert_equal "Please repeat the passphrase for your Private Key: ", + output.shift assert_equal "Certificate: #{File.join @tempdir, 'gem-public_cert.pem'}", output.shift assert_equal "Private Key: #{File.join @tempdir, 'gem-private_key.pem'}", @@ -115,12 +123,43 @@ Added '/CN=alternate/DC=example' output.shift assert_empty output - assert_empty @ui.error + assert_empty @build_ui.error assert_path_exists File.join(@tempdir, 'gem-private_key.pem') assert_path_exists File.join(@tempdir, 'gem-public_cert.pem') end + def test_execute_build_bad_passphrase_confirmation + passphrase = 'Foo bar' + passphrase_confirmation = 'Fu bar' + + @cmd.handle_options %W[--build nobody@example.com] + + @build_ui = Gem::MockGemUi.new "#{passphrase}\n#{passphrase_confirmation}" + + use_ui @build_ui do + e = assert_raises Gem::CommandLineError do + @cmd.execute + end + + output = @build_ui.output.split "\n" + + assert_equal "Passphrase for your Private Key: ", + output.shift + assert_equal "Please repeat the passphrase for your Private Key: ", + output.shift + + assert_empty output + + assert_equal "Passphrase and passphrase confirmation don't match", + e.message + + end + + refute_path_exists File.join(@tempdir, 'gem-private_key.pem') + refute_path_exists File.join(@tempdir, 'gem-public_cert.pem') + end + def test_execute_build_key @cmd.handle_options %W[ --build nobody@example.com @@ -135,21 +174,32 @@ Added '/CN=alternate/DC=example' assert_equal "Certificate: #{File.join @tempdir, 'gem-public_cert.pem'}", output.shift - assert_equal "Private Key: #{File.join @tempdir, 'gem-private_key.pem'}", - output.shift - assert_equal "Don't forget to move the key file to somewhere private!", + assert_empty output + assert_empty @ui.error + + assert_path_exists File.join(@tempdir, 'gem-public_cert.pem') + end + + def test_execute_build_encrypted_key + @cmd.handle_options %W[ + --build nobody@example.com + --private-key #{ENCRYPTED_PRIVATE_KEY_PATH} + ] + + use_ui @ui do + @cmd.execute + end + + output = @ui.output.split "\n" + + assert_equal "Certificate: #{File.join @tempdir, 'gem-public_cert.pem'}", output.shift assert_empty output assert_empty @ui.error assert_path_exists File.join(@tempdir, 'gem-public_cert.pem') - - private_key_file = File.join @tempdir, 'gem-private_key.pem' - assert_path_exists private_key_file - - assert_equal PRIVATE_KEY.to_pem, File.read(private_key_file) end def test_execute_certificate @@ -203,6 +253,17 @@ Added '/CN=alternate/DC=example' assert_equal PRIVATE_KEY.to_pem, @cmd.options[:key].to_pem end + def test_execute_encrypted_private_key + use_ui @ui do + @cmd.send :handle_options, %W[--private-key #{ENCRYPTED_PRIVATE_KEY_PATH}] + end + + assert_equal '', @ui.output + assert_equal '', @ui.error + + assert_equal ENCRYPTED_PRIVATE_KEY.to_pem, @cmd.options[:key].to_pem + end + def test_execute_remove @trust_dir.trust_cert PUBLIC_CERT @@ -307,6 +368,35 @@ Removed '/CN=alternate/DC=example' assert_equal mask, File.stat(path).mode unless win_platform? end + def test_execute_sign_encrypted_key + path = File.join @tempdir, 'cert.pem' + Gem::Security.write ALTERNATE_CERT, path, 0600 + + assert_equal '/CN=alternate/DC=example', ALTERNATE_CERT.issuer.to_s + + @cmd.handle_options %W[ + --private-key #{ENCRYPTED_PRIVATE_KEY_PATH} + --certificate #{PUBLIC_CERT_FILE} + + --sign #{path} + ] + + use_ui @ui do + @cmd.execute + end + + assert_equal '', @ui.output + assert_equal '', @ui.error + + cert = OpenSSL::X509::Certificate.new File.read path + + assert_equal '/CN=nobody/DC=example', cert.issuer.to_s + + mask = 0100600 & (~File.umask) + + assert_equal mask, File.stat(path).mode unless win_platform? + end + def test_execute_sign_default FileUtils.mkdir_p File.join Gem.user_home, '.gem' @@ -339,6 +429,38 @@ Removed '/CN=alternate/DC=example' assert_equal mask, File.stat(path).mode unless win_platform? end + def test_execute_sign_default_encrypted_key + FileUtils.mkdir_p File.join(Gem.user_home, '.gem') + + private_key_path = File.join Gem.user_home, '.gem', 'gem-private_key.pem' + Gem::Security.write ENCRYPTED_PRIVATE_KEY, private_key_path, 0600, PRIVATE_KEY_PASSPHRASE + + public_cert_path = File.join Gem.user_home, '.gem', 'gem-public_cert.pem' + Gem::Security.write PUBLIC_CERT, public_cert_path + + path = File.join @tempdir, 'cert.pem' + Gem::Security.write ALTERNATE_CERT, path, 0600 + + assert_equal '/CN=alternate/DC=example', ALTERNATE_CERT.issuer.to_s + + @cmd.handle_options %W[--sign #{path}] + + use_ui @ui do + @cmd.execute + end + + assert_equal '', @ui.output + assert_equal '', @ui.error + + cert = OpenSSL::X509::Certificate.new File.read path + + assert_equal '/CN=nobody/DC=example', cert.issuer.to_s + + mask = 0100600 & (~File.umask) + + assert_equal mask, File.stat(path).mode unless win_platform? + end + def test_execute_sign_no_cert FileUtils.mkdir_p File.join Gem.user_home, '.gem' @@ -509,6 +631,24 @@ ERROR: --private-key not specified and ~/.gem/gem-private_key.pem does not exis assert_equal [ALTERNATE_CERT_FILE, CHILD_CERT_FILE], @cmd.options[:sign] end + def test_handle_options_sign_encrypted_key + @cmd.handle_options %W[ + --private-key #{ALTERNATE_KEY_FILE} + --private-key #{ENCRYPTED_PRIVATE_KEY_PATH} + + --certificate #{ALTERNATE_CERT_FILE} + --certificate #{PUBLIC_CERT_FILE} + + --sign #{ALTERNATE_CERT_FILE} + --sign #{CHILD_CERT_FILE} + ] + + assert_equal ENCRYPTED_PRIVATE_KEY.to_pem, @cmd.options[:key].to_pem + assert_equal PUBLIC_CERT.to_pem, @cmd.options[:issuer_cert].to_pem + + assert_equal [ALTERNATE_CERT_FILE, CHILD_CERT_FILE], @cmd.options[:sign] + end + def test_handle_options_sign_nonexistent nonexistent = File.join @tempdir, 'nonexistent' e = assert_raises OptionParser::InvalidArgument do @@ -525,5 +665,5 @@ ERROR: --private-key not specified and ~/.gem/gem-private_key.pem does not exis e.message end -end if defined? OpenSSL +end if defined?(OpenSSL::SSL) diff --git a/test/rubygems/test_gem_commands_cleanup_command.rb b/test/rubygems/test_gem_commands_cleanup_command.rb index 26b1fe7b0c..6fd06d26e0 100644 --- a/test/rubygems/test_gem_commands_cleanup_command.rb +++ b/test/rubygems/test_gem_commands_cleanup_command.rb @@ -15,6 +15,21 @@ class TestGemCommandsCleanupCommand < Gem::TestCase install_gem @a_2 end + def test_handle_options_d + @cmd.handle_options %w[-d] + assert @cmd.options[:dryrun] + end + + def test_handle_options_dry_run + @cmd.handle_options %w[--dryrun] + assert @cmd.options[:dryrun] + end + + def test_handle_options_n + @cmd.handle_options %w[-n] + assert @cmd.options[:dryrun] + end + def test_execute @cmd.options[:args] = %w[a] diff --git a/test/rubygems/test_gem_commands_contents_command.rb b/test/rubygems/test_gem_commands_contents_command.rb index 60df53f53b..35c9631959 100644 --- a/test/rubygems/test_gem_commands_contents_command.rb +++ b/test/rubygems/test_gem_commands_contents_command.rb @@ -140,10 +140,10 @@ lib/foo.rb @cmd.execute end - expected = %W[ - #{Gem::ConfigMap[:bindir]}/default_command - #{Gem::ConfigMap[:rubylibdir]}/default/gem.rb - #{Gem::ConfigMap[:archdir]}/default_gem.so + expected = [ + File.join(Gem::ConfigMap[:bindir], 'default_command'), + File.join(Gem::ConfigMap[:rubylibdir], 'default/gem.rb'), + File.join(Gem::ConfigMap[:archdir], 'default_gem.so') ].sort.join "\n" assert_equal expected, @ui.output.chomp diff --git a/test/rubygems/test_gem_commands_environment_command.rb b/test/rubygems/test_gem_commands_environment_command.rb index 439057df9d..a97ed4360f 100644 --- a/test/rubygems/test_gem_commands_environment_command.rb +++ b/test/rubygems/test_gem_commands_environment_command.rb @@ -11,6 +11,7 @@ class TestGemCommandsEnvironmentCommand < Gem::TestCase def test_execute orig_sources = Gem.sources.dup + orig_path, ENV['PATH'] = ENV['PATH'], '/usr/local/bin:/usr/bin:/bin' Gem.sources.replace %w[http://gems.example.com] Gem.configuration['gemcutter_key'] = 'blah' @@ -36,10 +37,17 @@ class TestGemCommandsEnvironmentCommand < Gem::TestCase assert_match %r|"gemcutter_key" => "\*\*\*\*"|, @ui.output assert_match %r|:verbose => |, @ui.output assert_match %r|REMOTE SOURCES:|, @ui.output - assert_equal '', @ui.error + + assert_match %r|- SHELL PATH:|, @ui.output + assert_match %r|- /usr/local/bin$|, @ui.output + assert_match %r|- /usr/bin$|, @ui.output + assert_match %r|- /bin$|, @ui.output + + assert_empty @ui.error ensure Gem.sources.replace orig_sources + ENV['PATH'] = orig_path end def test_execute_gemdir diff --git a/test/rubygems/test_gem_commands_help_command.rb b/test/rubygems/test_gem_commands_help_command.rb index a10e575a5a..c3e143f619 100644 --- a/test/rubygems/test_gem_commands_help_command.rb +++ b/test/rubygems/test_gem_commands_help_command.rb @@ -36,9 +36,12 @@ class TestGemCommandsHelpCommand < Gem::TestCase mgr.command_names.each do |cmd| assert_match(/\s+#{cmd}\s+\S+/, out) end - assert_equal '', err - refute_match 'No command found for ', out + if defined?(OpenSSL::SSL) then + assert_empty err + + refute_match 'No command found for ', out + end end end diff --git a/test/rubygems/test_gem_commands_install_command.rb b/test/rubygems/test_gem_commands_install_command.rb index 28b4e474ba..8bfb633070 100644 --- a/test/rubygems/test_gem_commands_install_command.rb +++ b/test/rubygems/test_gem_commands_install_command.rb @@ -5,6 +5,7 @@ class TestGemCommandsInstallCommand < Gem::TestCase def setup super + common_installer_setup @cmd = Gem::Commands::InstallCommand.new @cmd.options[:document] = [] @@ -167,8 +168,7 @@ class TestGemCommandsInstallCommand < Gem::TestCase # This is needed because we need to exercise the cache path # within SpecFetcher - path = File.join Gem.user_home, '.gem', 'specs', "not-there.nothing%80", - "latest_specs.4.8" + path = File.join Gem.spec_cache_dir, "not-there.nothing%80", "latest_specs.4.8" FileUtils.mkdir_p File.dirname(path) @@ -632,67 +632,6 @@ ERROR: Possible alternatives: non_existent_with_hint assert_equal x, e end - def test_execute_installs_dependencies - r, r_gem = util_gem 'r', '1', 'q' => '= 1' - q, q_gem = util_gem 'q', '1' - - util_setup_fake_fetcher - util_setup_spec_fetcher r, q - - Gem::Specification.reset - - @fetcher.data["#{@gem_repo}gems/#{q.file_name}"] = read_binary(q_gem) - @fetcher.data["#{@gem_repo}gems/#{r.file_name}"] = read_binary(r_gem) - - @cmd.options[:args] = ["r"] - - e = nil - use_ui @ui do - e = assert_raises Gem::SystemExitException do - capture_io do - @cmd.execute - end - end - end - - out = @ui.output.split "\n" - assert_equal "2 gems installed", out.shift - assert out.empty?, out.inspect - - assert_equal %w[q-1 r-1], @cmd.installed_specs.map { |spec| spec.full_name } - - assert_equal 0, e.exit_code - end - - def test_execute_satisfy_deps_of_local_from_sources - r, r_gem = util_gem 'r', '1', 'q' => '= 1' - q, q_gem = util_gem 'q', '1' - - util_setup_fake_fetcher - util_setup_spec_fetcher r, q - - Gem::Specification.reset - - @fetcher.data["#{@gem_repo}gems/#{q.file_name}"] = read_binary(q_gem) - - @cmd.options[:args] = [r_gem] - - use_ui @ui do - e = assert_raises Gem::SystemExitException do - capture_io do - @cmd.execute - end - end - assert_equal 0, e.exit_code - end - - assert_equal %w[q-1 r-1], @cmd.installed_specs.map { |spec| spec.full_name } - - out = @ui.output.split "\n" - assert_equal "2 gems installed", out.shift - assert out.empty?, out.inspect - end - def test_execute_uses_from_a_gemdeps util_setup_fake_fetcher util_setup_spec_fetcher @@ -950,4 +889,3 @@ ERROR: Possible alternatives: non_existent_with_hint end - diff --git a/test/rubygems/test_gem_commands_owner_command.rb b/test/rubygems/test_gem_commands_owner_command.rb index dfbc2572dc..5d7b66137e 100644 --- a/test/rubygems/test_gem_commands_owner_command.rb +++ b/test/rubygems/test_gem_commands_owner_command.rb @@ -6,6 +6,7 @@ class TestGemCommandsOwnerCommand < Gem::TestCase def setup super + ENV["RUBYGEMS_HOST"] = nil @fetcher = Gem::FakeFetcher.new Gem::RemoteFetcher.fetcher = @fetcher Gem.configuration.rubygems_api_key = "ed244fbf2b1a52e012da8616c512fa47f9aa5250" @@ -34,6 +35,36 @@ EOF assert_match %r{- user2@example.com}, @ui.output end + def test_show_owners_setting_up_host_through_env_var + response = "- email: user1@example.com\n" + host = "http://rubygems.example" + ENV["RUBYGEMS_HOST"] = host + + @fetcher.data["#{host}/api/v1/gems/freewill/owners.yaml"] = [response, 200, 'OK'] + + use_ui @ui do + @cmd.show_owners("freewill") + end + + assert_match %r{Owners for gem: freewill}, @ui.output + assert_match %r{- user1@example.com}, @ui.output + end + + def test_show_owners_setting_up_host + response = "- email: user1@example.com\n" + host = "http://rubygems.example" + @cmd.host = host + + @fetcher.data["#{host}/api/v1/gems/freewill/owners.yaml"] = [response, 200, 'OK'] + + use_ui @ui do + @cmd.show_owners("freewill") + end + + assert_match %r{Owners for gem: freewill}, @ui.output + assert_match %r{- user1@example.com}, @ui.output + end + def test_show_owners_denied response = "You don't have permission to push to this gem" @fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners.yaml"] = [response, 403, 'Forbidden'] @@ -87,6 +118,24 @@ EOF assert_match response, @ui.output end + def test_add_owner_with_host_option_through_execute + host = "http://rubygems.example" + add_owner_response = "Owner added successfully." + show_owners_response = "- email: user1@example.com\n" + @fetcher.data["#{host}/api/v1/gems/freewill/owners"] = [add_owner_response, 200, 'OK'] + @fetcher.data["#{host}/api/v1/gems/freewill/owners.yaml"] = [show_owners_response, 200, 'OK'] + + @cmd.handle_options %W[--host #{host} --add user-new1@example.com freewill] + + use_ui @ui do + @cmd.execute + end + + assert_match add_owner_response, @ui.output + assert_match %r{Owners for gem: freewill}, @ui.output + assert_match %r{- user1@example.com}, @ui.output + end + def test_add_owners_key response = "Owner added successfully." @fetcher.data["#{Gem.host}/api/v1/gems/freewill/owners"] = [response, 200, 'OK'] diff --git a/test/rubygems/test_gem_commands_pristine_command.rb b/test/rubygems/test_gem_commands_pristine_command.rb index 778ce2ee1f..425f0eae1e 100644 --- a/test/rubygems/test_gem_commands_pristine_command.rb +++ b/test/rubygems/test_gem_commands_pristine_command.rb @@ -80,6 +80,32 @@ class TestGemCommandsPristineCommand < Gem::TestCase assert_empty out, out.inspect end + def test_execute_env_shebang + a = quick_spec 'a' do |s| + s.executables = %w[foo] + s.files = %w[bin/foo] + end + write_file File.join(@tempdir, 'bin', 'foo') do |fp| + fp.puts "#!/usr/bin/ruby" + end + + install_gem a + + gem_exec = File.join @gemhome, 'bin', 'foo' + + FileUtils.rm gem_exec + + @cmd.handle_options %w[--all --env-shebang] + + use_ui @ui do + @cmd.execute + end + + assert_path_exists gem_exec + + assert_match '/usr/bin/env', File.read(gem_exec) + end + def test_execute_no_extension a = quick_spec 'a' do |s| s.extensions << 'ext/a/extconf.rb' end diff --git a/test/rubygems/test_gem_commands_sources_command.rb b/test/rubygems/test_gem_commands_sources_command.rb index 8dc2f91ed4..1e5afca009 100644 --- a/test/rubygems/test_gem_commands_sources_command.rb +++ b/test/rubygems/test_gem_commands_sources_command.rb @@ -194,7 +194,7 @@ beta-gems.example.com is not a URI assert_equal expected, @ui.output assert_equal '', @ui.error - dir = File.join Gem.user_home, '.gem', 'specs' + dir = Gem.spec_cache_dir refute File.exist?(dir), 'cache dir removed' end diff --git a/test/rubygems/test_gem_commands_update_command.rb b/test/rubygems/test_gem_commands_update_command.rb index 006d821210..d1c1b20c6e 100644 --- a/test/rubygems/test_gem_commands_update_command.rb +++ b/test/rubygems/test_gem_commands_update_command.rb @@ -11,6 +11,7 @@ class TestGemCommandsUpdateCommand < Gem::TestCase def setup super + common_installer_setup @cmd = Gem::Commands::UpdateCommand.new @@ -253,7 +254,7 @@ class TestGemCommandsUpdateCommand < Gem::TestCase out = @ui.output.split "\n" assert_equal "Updating installed gems", out.shift assert_equal "Updating #{@a2.name}", out.shift - assert_equal "Gems updated: #{@c2.name} #{@b2.name} #{@a2.name}", + assert_equal "Gems updated: #{@a2.name} #{@b2.name} #{@c2.name}", out.shift assert_empty out diff --git a/test/rubygems/test_gem_config_file.rb b/test/rubygems/test_gem_config_file.rb index 13f1c34b50..e9cd33579d 100644 --- a/test/rubygems/test_gem_config_file.rb +++ b/test/rubygems/test_gem_config_file.rb @@ -201,6 +201,10 @@ ERROR: Your gem push credentials file located at: has file permissions of 0644 but 0600 is required. +To fix this error run: + +\tchmod 0600 #{@cfg.credentials_path} + You should reset your credentials at: \thttps://rubygems.org/profile/edit @@ -428,6 +432,14 @@ if you believe they were disclosed to a third party. assert_equal('/home/me/certs', @cfg.ssl_ca_cert) end + def test_load_ssl_client_cert_from_config + File.open @temp_conf, 'w' do |fp| + fp.puts ":ssl_client_cert: /home/me/mine.pem" + end + util_config_file + assert_equal('/home/me/mine.pem', @cfg.ssl_client_cert) + end + def util_config_file(args = @cfg_args) @cfg = Gem::ConfigFile.new args end diff --git a/test/rubygems/test_gem_dependency_installer.rb b/test/rubygems/test_gem_dependency_installer.rb index 2b95597d4b..af6b922fe3 100644 --- a/test/rubygems/test_gem_dependency_installer.rb +++ b/test/rubygems/test_gem_dependency_installer.rb @@ -6,6 +6,7 @@ class TestGemDependencyInstaller < Gem::TestCase def setup super + common_installer_setup @gems_dir = File.join @tempdir, 'gems' @cache_dir = File.join @gemhome, 'cache' @@ -172,7 +173,8 @@ class TestGemDependencyInstaller < Gem::TestCase FileUtils.mv @a1_gem, @tempdir FileUtils.mv @b1_gem, @tempdir - FileUtils.mv e1_gem, @tempdir + FileUtils.mv e1_gem, @tempdir + inst = nil Dir.chdir @tempdir do @@ -180,40 +182,15 @@ class TestGemDependencyInstaller < Gem::TestCase inst.install 'b' end + assert_equal %w[b-1], inst.installed_gems.map { |s| s.full_name }, + 'sanity check' + Dir.chdir @tempdir do inst = Gem::DependencyInstaller.new inst.install 'e' end - assert_equal %w[e-1 a-1], inst.installed_gems.map { |s| s.full_name } - end - - def test_install_ignore_satified_deps - util_setup_gems - - _, e1_gem = util_gem 'e', '1' do |s| - s.add_dependency 'b' - end - - util_clear_gems - - FileUtils.mv @a1_gem, @tempdir - FileUtils.mv @b1_gem, @tempdir - FileUtils.mv e1_gem, @tempdir - - Dir.chdir @tempdir do - i = Gem::DependencyInstaller.new :ignore_dependencies => true - i.install 'b' - end - - inst = nil - - Dir.chdir @tempdir do - inst = Gem::DependencyInstaller.new :minimal_deps => true - inst.install 'e' - end - - assert_equal %w[e-1], inst.installed_gems.map { |s| s.full_name } + assert_equal %w[a-1 e-1], inst.installed_gems.map { |s| s.full_name } end def test_install_cache_dir @@ -246,15 +223,18 @@ class TestGemDependencyInstaller < Gem::TestCase Gem::Specification.reset FileUtils.mv @a1_gem, @tempdir - FileUtils.mv a2_gem, @tempdir # not in index + FileUtils.mv a2_gem, @tempdir # not in index FileUtils.mv @b1_gem, @tempdir inst = nil Dir.chdir @tempdir do inst = Gem::DependencyInstaller.new - inst.install 'a', Gem::Requirement.create("= 2") + inst.install 'a', req("= 2") end + assert_equal %w[a-2], inst.installed_gems.map { |s| s.full_name }, + 'sanity check' + FileUtils.rm File.join(@tempdir, a2.file_name) Dir.chdir @tempdir do @@ -282,19 +262,18 @@ class TestGemDependencyInstaller < Gem::TestCase Gem::Specification.reset FileUtils.mv @a1_gem, @tempdir - FileUtils.mv a2_gem, @tempdir # not in index + FileUtils.mv a2_gem, @tempdir # not in index FileUtils.mv @b1_gem, @tempdir - FileUtils.mv a3_gem, @tempdir - - inst = nil + FileUtils.mv a3_gem, @tempdir Dir.chdir @tempdir do - inst = Gem::DependencyInstaller.new - inst.install 'a', Gem::Requirement.create("= 2") + Gem::DependencyInstaller.new.install 'a', req("= 2") end FileUtils.rm File.join(@tempdir, a2.file_name) + inst = nil + Dir.chdir @tempdir do inst = Gem::DependencyInstaller.new inst.install 'b' @@ -488,6 +467,42 @@ class TestGemDependencyInstaller < Gem::TestCase assert_equal %w[a-1], inst.installed_gems.map { |s| s.full_name } end + def test_install_minimal_deps + util_setup_gems + + _, e1_gem = util_gem 'e', '1' do |s| + s.add_dependency 'b' + end + + _, b2_gem = util_gem 'b', '2' do |s| + s.add_dependency 'a' + end + + util_clear_gems + + FileUtils.mv @a1_gem, @tempdir + FileUtils.mv @b1_gem, @tempdir + FileUtils.mv b2_gem, @tempdir + FileUtils.mv e1_gem, @tempdir + + inst = nil + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new :ignore_dependencies => true + inst.install 'b', req('= 1') + end + + assert_equal %w[b-1], inst.installed_gems.map { |s| s.full_name }, + 'sanity check' + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new :minimal_deps => true + inst.install 'e' + end + + assert_equal %w[a-1 e-1], inst.installed_gems.map { |s| s.full_name } + end + def test_install_env_shebang util_setup_gems @@ -627,12 +642,12 @@ class TestGemDependencyInstaller < Gem::TestCase inst = nil Dir.chdir @tempdir do - e = assert_raises Gem::DependencyError do + e = assert_raises Gem::UnsatisfiableDependencyError do inst = Gem::DependencyInstaller.new :domain => :local inst.install 'b' end - expected = "Unable to resolve dependencies: b requires a (>= 0)" + expected = "Unable to resolve dependency: b (= 1) requires a (>= 0)" assert_equal expected, e.message end @@ -910,12 +925,13 @@ class TestGemDependencyInstaller < Gem::TestCase gems = set.sorted assert_equal 2, gems.length - local = gems.first + + remote, local = gems + assert_equal 'a-1', local.spec.full_name, 'local spec' assert_equal File.join(@tempdir, @a1.file_name), local.source.download(local.spec), 'local path' - remote = gems.last assert_equal 'a-1', remote.spec.full_name, 'remote spec' assert_equal Gem::Source.new(@gem_repo), remote.source, 'remote path' diff --git a/test/rubygems/test_gem_dependency_resolver.rb b/test/rubygems/test_gem_dependency_resolver.rb index 08d8e62f2b..3f65c23aa9 100644 --- a/test/rubygems/test_gem_dependency_resolver.rb +++ b/test/rubygems/test_gem_dependency_resolver.rb @@ -15,7 +15,9 @@ class TestGemDependencyResolver < Gem::TestCase exp = expected.sort_by { |s| s.full_name } act = actual.map { |a| a.spec }.sort_by { |s| s.full_name } - assert_equal exp, act + msg = "Set of gems was not the same: #{exp.map { |x| x.full_name}.inspect} != #{act.map { |x| x.full_name}.inspect}" + + assert_equal exp, act, msg end def test_no_overlap_specificly @@ -177,7 +179,8 @@ class TestGemDependencyResolver < Gem::TestCase r.resolve end - assert_equal "unable to find any gem matching dependency 'a (>= 0)'", e.message + assert_equal "Unable to resolve dependency: (unknown) requires a (>= 0)", + e.message assert_equal "a (>= 0)", e.dependency.to_s end @@ -215,7 +218,7 @@ class TestGemDependencyResolver < Gem::TestCase r.resolve end - assert_equal "detected 1 conflict with dependency 'c (>= 2)'", e.message + assert_match "a-1 requires c (>= 2) but it conflicted", e.message assert_equal "c (>= 2)", e.dependency.to_s diff --git a/test/rubygems/test_gem_dependency_resolver_dependency_conflict.rb b/test/rubygems/test_gem_dependency_resolver_dependency_conflict.rb new file mode 100644 index 0000000000..56a34d3001 --- /dev/null +++ b/test/rubygems/test_gem_dependency_resolver_dependency_conflict.rb @@ -0,0 +1,36 @@ +require 'rubygems/test_case' +require 'rubygems/dependency_resolver' + +class TestGemDependencyResolverDependencyConflict < Gem::TestCase + + def test_explanation + root = + dependency_request dep('net-ssh', '>= 2.0.13'), 'rye', '0.9.8' + child = + dependency_request dep('net-ssh', '>= 2.6.5'), 'net-ssh', '2.2.2', root + + conflict = + Gem::DependencyResolver::DependencyConflict.new child, child.requester + + expected = <<-EXPECTED + Activated net-ssh-2.2.2 instead of (>= 2.6.5) via: + net-ssh-2.2.2, rye-0.9.8 + EXPECTED + + assert_equal expected, conflict.explanation + end + + def test_request_path + root = + dependency_request dep('net-ssh', '>= 2.0.13'), 'rye', '0.9.8' + child = + dependency_request dep('net-ssh', '>= 2.6.5'), 'net-ssh', '2.2.2', root + + conflict = + Gem::DependencyResolver::DependencyConflict.new child, nil + + assert_equal %w[net-ssh-2.2.2 rye-0.9.8], conflict.request_path + end + +end + diff --git a/test/rubygems/test_gem_gem_runner.rb b/test/rubygems/test_gem_gem_runner.rb index 6680a784c9..85ff725345 100644 --- a/test/rubygems/test_gem_gem_runner.rb +++ b/test/rubygems/test_gem_gem_runner.rb @@ -7,6 +7,7 @@ class TestGemGemRunner < Gem::TestCase super @orig_args = Gem::Command.build_args + @runner = Gem::GemRunner.new end def teardown @@ -41,23 +42,26 @@ class TestGemGemRunner < Gem::TestCase assert_equal %w[--commands], Gem::Command.extra_args end - def test_build_args_are_handled - Gem.clear_paths + def test_extract_build_args + args = %w[] + assert_equal [], @runner.extract_build_args(args) + assert_equal %w[], args - cls = Class.new(Gem::Command) do - def execute - end - end + args = %w[foo] + assert_equal [], @runner.extract_build_args(args) + assert_equal %w[foo], args - test_obj = cls.new :ba_test + args = %w[--foo] + assert_equal [], @runner.extract_build_args(args) + assert_equal %w[--foo], args - cmds = Gem::CommandManager.new - cmds.register_command :ba_test, test_obj + args = %w[--foo --] + assert_equal [], @runner.extract_build_args(args) + assert_equal %w[--foo], args - runner = Gem::GemRunner.new :command_manager => cmds - runner.run(%W[ba_test -- --build_arg1 --build_arg2]) - - assert_equal %w[--build_arg1 --build_arg2], test_obj.options[:build_args] + args = %w[--foo -- --bar] + assert_equal %w[--bar], @runner.extract_build_args(args) + assert_equal %w[--foo], args end end diff --git a/test/rubygems/test_gem_impossible_dependencies_error.rb b/test/rubygems/test_gem_impossible_dependencies_error.rb new file mode 100644 index 0000000000..9a0472fa36 --- /dev/null +++ b/test/rubygems/test_gem_impossible_dependencies_error.rb @@ -0,0 +1,41 @@ +require 'rubygems/test_case' + +class TestGemImpossibleDependenciesError < Gem::TestCase + + def test_message_conflict + request = dependency_request dep('net-ssh', '>= 2.0.13'), 'rye', '0.9.8' + + conflicts = [] + + # These conflicts are lies as their dependencies does not have the correct + # requested-by entries, but they are suitable for testing the message. + # See #485 to construct a correct conflict. + net_ssh_2_2_2 = + dependency_request dep('net-ssh', '>= 2.6.5'), 'net-ssh', '2.2.2', request + net_ssh_2_6_5 = + dependency_request dep('net-ssh', '~> 2.2.2'), 'net-ssh', '2.6.5', request + + conflict1 = Gem::DependencyResolver::DependencyConflict.new \ + net_ssh_2_6_5, net_ssh_2_6_5.requester + + conflict2 = Gem::DependencyResolver::DependencyConflict.new \ + net_ssh_2_2_2, net_ssh_2_2_2.requester + + conflicts << [net_ssh_2_6_5.requester.spec, conflict1] + conflicts << [net_ssh_2_2_2.requester.spec, conflict2] + + error = Gem::ImpossibleDependenciesError.new request, conflicts + + expected = <<-EXPECTED +rye-0.9.8 requires net-ssh (>= 2.0.13) but it conflicted: + Activated net-ssh-2.6.5 instead of (~> 2.2.2) via: + net-ssh-2.6.5, rye-0.9.8 + Activated net-ssh-2.2.2 instead of (>= 2.6.5) via: + net-ssh-2.2.2, rye-0.9.8 + EXPECTED + + assert_equal expected, error.message + end + +end + diff --git a/test/rubygems/test_gem_install_update_options.rb b/test/rubygems/test_gem_install_update_options.rb index 18d3569417..3f63896999 100644 --- a/test/rubygems/test_gem_install_update_options.rb +++ b/test/rubygems/test_gem_install_update_options.rb @@ -22,12 +22,13 @@ class TestGemInstallUpdateOptions < Gem::InstallerTestCase --rdoc --ri -E - -P HighSecurity -f -i /install_to -w ] + args.concat %w[-P HighSecurity] if defined?(OpenSSL::SSL) + assert @cmd.handles?(args) end @@ -100,6 +101,8 @@ class TestGemInstallUpdateOptions < Gem::InstallerTestCase end def test_security_policy + skip 'openssl is missing' unless defined?(OpenSSL::SSL) + @cmd.handle_options %w[-P HighSecurity] assert_equal Gem::Security::HighSecurity, @cmd.options[:security_policy] diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index e513fe6da3..fbc7cd05d4 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -4,6 +4,7 @@ class TestGemInstaller < Gem::InstallerTestCase def setup super + common_installer_setup if __name__ =~ /^test_install(_|$)/ then FileUtils.rm_r @spec.gem_dir @@ -14,6 +15,8 @@ class TestGemInstaller < Gem::InstallerTestCase end def teardown + common_installer_teardown + super Gem.configuration = @config @@ -300,6 +303,8 @@ gem 'other', version end def test_ensure_loadable_spec_security_policy + skip 'openssl is missing' unless defined?(OpenSSL::SSL) + _, a_gem = util_gem 'a', 2 do |s| s.add_dependency 'garbage ~> 5' end @@ -1341,7 +1346,7 @@ gem 'other', version assert File.exist?(File.join(dest, 'bin', 'executable')) end - def test_write_build_args + def test_write_build_info_file refute_path_exists @spec.build_info_file @installer.build_args = %w[ @@ -1357,7 +1362,7 @@ gem 'other', version assert_equal expected, File.read(@spec.build_info_file) end - def test_write_build_args_empty + def test_write_build_info_file_empty refute_path_exists @spec.build_info_file @installer.write_build_info_file @@ -1429,6 +1434,30 @@ gem 'other', version assert_match %r!/gemhome/gems/a-2$!, @installer.dir end + def test_default_gem + FileUtils.rm_f File.join(Gem.dir, 'specifications') + + @installer.wrappers = true + @installer.options[:install_as_default] = true + @installer.gem_dir = util_gem_dir @spec + @installer.generate_bin + + use_ui @ui do + @installer.install + end + + assert File.directory? util_inst_bindir + installed_exec = File.join util_inst_bindir, 'executable' + assert File.exist? installed_exec + + assert File.directory? File.join(Gem.dir, 'specifications') + assert File.directory? File.join(Gem.dir, 'specifications', 'default') + + default_spec = eval File.read File.join(Gem.dir, 'specifications', 'default', 'a-2.gemspec') + assert_equal Gem::Version.new("2"), default_spec.version + assert_equal ['bin/executable'], default_spec.files + end + def old_ruby_required spec = quick_spec 'old_ruby_required', '1' do |s| s.required_ruby_version = '= 1.4.6' diff --git a/test/rubygems/test_gem_name_tuple.rb b/test/rubygems/test_gem_name_tuple.rb index 62b4801f71..170a9c2ae0 100644 --- a/test/rubygems/test_gem_name_tuple.rb +++ b/test/rubygems/test_gem_name_tuple.rb @@ -2,6 +2,21 @@ require 'rubygems/test_case' require 'rubygems/name_tuple' class TestGemNameTuple < Gem::TestCase + + def test_full_name + n = Gem::NameTuple.new "a", Gem::Version.new(0), "ruby" + assert_equal "a-0", n.full_name + + n = Gem::NameTuple.new "a", Gem::Version.new(0), nil + assert_equal "a-0", n.full_name + + n = Gem::NameTuple.new "a", Gem::Version.new(0), "" + assert_equal "a-0", n.full_name + + n = Gem::NameTuple.new "a", Gem::Version.new(0), "other" + assert_equal "a-0-other", n.full_name + end + def test_platform_normalization n = Gem::NameTuple.new "a", Gem::Version.new(0), "ruby" assert_equal "ruby", n.platform @@ -12,4 +27,11 @@ class TestGemNameTuple < Gem::TestCase n = Gem::NameTuple.new "a", Gem::Version.new(0), "" assert_equal "ruby", n.platform end + + def test_spec_name + n = Gem::NameTuple.new "a", Gem::Version.new(0), "ruby" + assert_equal "a-0.gemspec", n.spec_name + end + end + diff --git a/test/rubygems/test_gem_package.rb b/test/rubygems/test_gem_package.rb index bb78829bf9..55bb32af04 100644 --- a/test/rubygems/test_gem_package.rb +++ b/test/rubygems/test_gem_package.rb @@ -89,16 +89,19 @@ class TestGemPackage < Gem::Package::TarTestCase end expected = { - 'SHA1' => { - 'metadata.gz' => metadata_sha1, - 'data.tar.gz' => data_digests['SHA1'].hexdigest, - }, 'SHA512' => { 'metadata.gz' => metadata_sha512, 'data.tar.gz' => data_digests['SHA512'].hexdigest, } } + if defined?(OpenSSL::Digest) then + expected['SHA1'] = { + 'metadata.gz' => metadata_sha1, + 'data.tar.gz' => data_digests['SHA1'].hexdigest, + } + end + assert_equal expected, YAML.load(checksums) end @@ -162,11 +165,56 @@ class TestGemPackage < Gem::Package::TarTestCase end def test_build_auto_signed + skip 'openssl is missing' unless defined?(OpenSSL::SSL) + FileUtils.mkdir_p File.join(Gem.user_home, '.gem') private_key_path = File.join Gem.user_home, '.gem', 'gem-private_key.pem' Gem::Security.write PRIVATE_KEY, private_key_path + public_cert_path = File.join Gem.user_home, '.gem', 'gem-public_cert.pem' + FileUtils.cp PUBLIC_CERT_PATH, public_cert_path + + spec = Gem::Specification.new 'build', '1' + spec.summary = 'build' + spec.authors = 'build' + spec.files = ['lib/code.rb'] + + FileUtils.mkdir 'lib' + + open 'lib/code.rb', 'w' do |io| + io.write '# lib/code.rb' + end + + package = Gem::Package.new spec.file_name + package.spec = spec + + package.build + + assert_equal Gem::VERSION, spec.rubygems_version + assert_path_exists spec.file_name + + reader = Gem::Package.new spec.file_name + assert reader.verify + + assert_equal [PUBLIC_CERT.to_pem], reader.spec.cert_chain + + assert_equal %w[metadata.gz metadata.gz.sig + data.tar.gz data.tar.gz.sig + checksums.yaml.gz checksums.yaml.gz.sig], + reader.files + + assert_equal %w[lib/code.rb], reader.contents + end + + def test_build_auto_signed_encrypted_key + skip 'openssl is missing' unless defined?(OpenSSL::SSL) + + FileUtils.mkdir_p File.join(Gem.user_home, '.gem') + + private_key_path = File.join Gem.user_home, '.gem', 'gem-private_key.pem' + FileUtils.cp ENCRYPTED_PRIVATE_KEY_PATH, private_key_path + public_cert_path = File.join Gem.user_home, '.gem', 'gem-public_cert.pem' Gem::Security.write PUBLIC_CERT, public_cert_path @@ -216,6 +264,8 @@ class TestGemPackage < Gem::Package::TarTestCase end def test_build_signed + skip 'openssl is missing' unless defined?(OpenSSL::SSL) + spec = Gem::Specification.new 'build', '1' spec.summary = 'build' spec.authors = 'build' @@ -250,6 +300,43 @@ class TestGemPackage < Gem::Package::TarTestCase assert_equal %w[lib/code.rb], reader.contents end + def test_build_signed_encryped_key + skip 'openssl is missing' unless defined?(OpenSSL::SSL) + + spec = Gem::Specification.new 'build', '1' + spec.summary = 'build' + spec.authors = 'build' + spec.files = ['lib/code.rb'] + spec.cert_chain = [PUBLIC_CERT.to_pem] + spec.signing_key = ENCRYPTED_PRIVATE_KEY + + FileUtils.mkdir 'lib' + + open 'lib/code.rb', 'w' do |io| + io.write '# lib/code.rb' + end + + package = Gem::Package.new spec.file_name + package.spec = spec + + package.build + + assert_equal Gem::VERSION, spec.rubygems_version + assert_path_exists spec.file_name + + reader = Gem::Package.new spec.file_name + assert reader.verify + + assert_equal spec, reader.spec + + assert_equal %w[metadata.gz metadata.gz.sig + data.tar.gz data.tar.gz.sig + checksums.yaml.gz checksums.yaml.gz.sig], + reader.files + + assert_equal %w[lib/code.rb], reader.contents + end + def test_contents package = Gem::Package.new @gem @@ -446,7 +533,7 @@ class TestGemPackage < Gem::Package::TarTestCase io.write metadata_gz end - digest = OpenSSL::Digest::SHA1.new + digest = Digest::SHA1.new digest << metadata_gz checksums = { @@ -478,7 +565,8 @@ class TestGemPackage < Gem::Package::TarTestCase def test_verify_corrupt Tempfile.open 'corrupt' do |io| data = Gem.gzip 'a' * 10 - io.write tar_file_header('metadata.gz', "\000x", 0644, data.length) + io.write \ + tar_file_header('metadata.gz', "\000x", 0644, data.length, Time.now) io.write data io.rewind @@ -517,6 +605,8 @@ class TestGemPackage < Gem::Package::TarTestCase end def test_verify_security_policy + skip 'openssl is missing' unless defined?(OpenSSL::SSL) + package = Gem::Package.new @gem package.security_policy = Gem::Security::HighSecurity @@ -532,6 +622,8 @@ class TestGemPackage < Gem::Package::TarTestCase end def test_verify_security_policy_low_security + skip 'openssl is missing' unless defined?(OpenSSL::SSL) + @spec.cert_chain = [PUBLIC_CERT.to_pem] @spec.signing_key = PRIVATE_KEY @@ -550,6 +642,8 @@ class TestGemPackage < Gem::Package::TarTestCase end def test_verify_security_policy_checksum_missing + skip 'openssl is missing' unless defined?(OpenSSL::SSL) + @spec.cert_chain = [PUBLIC_CERT.to_pem] @spec.signing_key = PRIVATE_KEY @@ -605,6 +699,21 @@ class TestGemPackage < Gem::Package::TarTestCase e.message end + # end #verify tests + + def test_verify_entry + entry = Object.new + def entry.full_name() raise ArgumentError, 'whatever' end + + package = Gem::Package.new @gem + + e = assert_raises Gem::Package::FormatError do + package.verify_entry entry + end + + assert_equal "package is corrupt, exception while verifying: whatever (ArgumentError) in #{@gem}", e.message + end + def test_spec package = Gem::Package.new @gem diff --git a/test/rubygems/test_gem_package_old.rb b/test/rubygems/test_gem_package_old.rb index 05f7ae3dec..6236dbbaf2 100644 --- a/test/rubygems/test_gem_package_old.rb +++ b/test/rubygems/test_gem_package_old.rb @@ -21,6 +21,8 @@ class TestGemPackageOld < Gem::TestCase end def test_contents_security_policy + skip 'openssl is missing' unless defined?(OpenSSL::SSL) + @package.security_policy = Gem::Security::AlmostNoSecurity assert_raises Gem::Security::Exception do @@ -40,6 +42,8 @@ class TestGemPackageOld < Gem::TestCase end def test_extract_files_security_policy + skip 'openssl is missing' unless defined?(OpenSSL::SSL) + @package.security_policy = Gem::Security::AlmostNoSecurity assert_raises Gem::Security::Exception do @@ -52,6 +56,8 @@ class TestGemPackageOld < Gem::TestCase end def test_spec_security_policy + skip 'openssl is missing' unless defined?(OpenSSL::SSL) + @package.security_policy = Gem::Security::AlmostNoSecurity assert_raises Gem::Security::Exception do @@ -60,6 +66,8 @@ class TestGemPackageOld < Gem::TestCase end def test_verify + skip 'openssl is missing' unless defined?(OpenSSL::SSL) + assert @package.verify @package.security_policy = Gem::Security::NoSecurity diff --git a/test/rubygems/test_gem_package_tar_reader.rb b/test/rubygems/test_gem_package_tar_reader.rb index d1af22d3be..5e0474c253 100644 --- a/test/rubygems/test_gem_package_tar_reader.rb +++ b/test/rubygems/test_gem_package_tar_reader.rb @@ -4,8 +4,8 @@ require 'rubygems/package' class TestGemPackageTarReader < Gem::Package::TarTestCase def test_each_entry - tar = tar_dir_header "foo", "bar", 0 - tar << tar_file_header("bar", "baz", 0, 0) + tar = tar_dir_header "foo", "bar", 0, Time.now + tar << tar_file_header("bar", "baz", 0, 0, Time.now) io = TempIO.new tar @@ -25,8 +25,9 @@ class TestGemPackageTarReader < Gem::Package::TarTestCase def test_rewind content = ('a'..'z').to_a.join(" ") - str = tar_file_header("lib/foo", "", 010644, content.size) + content + - "\0" * (512 - content.size) + str = + tar_file_header("lib/foo", "", 010644, content.size, Time.now) + + content + "\0" * (512 - content.size) str << "\0" * 1024 Gem::Package::TarReader.new(TempIO.new(str)) do |tar_reader| @@ -43,8 +44,8 @@ class TestGemPackageTarReader < Gem::Package::TarTestCase end def test_seek - tar = tar_dir_header "foo", "bar", 0 - tar << tar_file_header("bar", "baz", 0, 0) + tar = tar_dir_header "foo", "bar", 0, Time.now + tar << tar_file_header("bar", "baz", 0, 0, Time.now) io = TempIO.new tar @@ -60,8 +61,8 @@ class TestGemPackageTarReader < Gem::Package::TarTestCase end def test_seek_missing - tar = tar_dir_header "foo", "bar", 0 - tar << tar_file_header("bar", "baz", 0, 0) + tar = tar_dir_header "foo", "bar", 0, Time.now + tar << tar_file_header("bar", "baz", 0, 0, Time.now) io = TempIO.new tar diff --git a/test/rubygems/test_gem_package_tar_reader_entry.rb b/test/rubygems/test_gem_package_tar_reader_entry.rb index 92da220fa6..3c1bf7291a 100644 --- a/test/rubygems/test_gem_package_tar_reader_entry.rb +++ b/test/rubygems/test_gem_package_tar_reader_entry.rb @@ -9,7 +9,7 @@ class TestGemPackageTarReaderEntry < Gem::Package::TarTestCase @contents = ('a'..'z').to_a.join * 100 @tar = '' - @tar << tar_file_header("lib/foo", "", 0, @contents.size) + @tar << tar_file_header("lib/foo", "", 0, @contents.size, Time.now) @tar << @contents @tar << "\0" * (512 - (@tar.size % 512)) diff --git a/test/rubygems/test_gem_package_tar_writer.rb b/test/rubygems/test_gem_package_tar_writer.rb index 40c6982929..2505d7ced1 100644 --- a/test/rubygems/test_gem_package_tar_writer.rb +++ b/test/rubygems/test_gem_package_tar_writer.rb @@ -1,5 +1,6 @@ require 'rubygems/package/tar_test_case' require 'rubygems/package/tar_writer' +require 'minitest/mock' class TestGemPackageTarWriter < Gem::Package::TarTestCase @@ -18,112 +19,130 @@ class TestGemPackageTarWriter < Gem::Package::TarTestCase end def test_add_file - @tar_writer.add_file 'x', 0644 do |f| f.write 'a' * 10 end + Time.stub :now, Time.at(1458518157) do + @tar_writer.add_file 'x', 0644 do |f| f.write 'a' * 10 end - assert_headers_equal(tar_file_header('x', '', 0644, 10), + assert_headers_equal(tar_file_header('x', '', 0644, 10, Time.now), @io.string[0, 512]) + end assert_equal "aaaaaaaaaa#{"\0" * 502}", @io.string[512, 512] assert_equal 1024, @io.pos end def test_add_file_digest - digest_algorithms = OpenSSL::Digest::SHA1, OpenSSL::Digest::SHA512 + digest_algorithms = Digest::SHA1, Digest::SHA512 - digests = @tar_writer.add_file_digest 'x', 0644, digest_algorithms do |io| - io.write 'a' * 10 - end + Time.stub :now, Time.at(1458518157) do + digests = @tar_writer.add_file_digest 'x', 0644, digest_algorithms do |io| + io.write 'a' * 10 + end - assert_equal '3495ff69d34671d1e15b33a63c1379fdedd3a32a', - digests['SHA1'].hexdigest - assert_equal '4714870aff6c97ca09d135834fdb58a6389a50c1' \ - '1fef8ec4afef466fb60a23ac6b7a9c92658f14df' \ - '4993d6b40a4e4d8424196afc347e97640d68de61' \ - 'e1cf14b0', - digests['SHA512'].hexdigest + assert_equal '3495ff69d34671d1e15b33a63c1379fdedd3a32a', + digests['SHA1'].hexdigest + assert_equal '4714870aff6c97ca09d135834fdb58a6389a50c1' \ + '1fef8ec4afef466fb60a23ac6b7a9c92658f14df' \ + '4993d6b40a4e4d8424196afc347e97640d68de61' \ + 'e1cf14b0', + digests['SHA512'].hexdigest - assert_headers_equal(tar_file_header('x', '', 0644, 10), + assert_headers_equal(tar_file_header('x', '', 0644, 10, Time.now), @io.string[0, 512]) + end assert_equal "aaaaaaaaaa#{"\0" * 502}", @io.string[512, 512] assert_equal 1024, @io.pos end def test_add_file_digest_multiple - digest_algorithms = [OpenSSL::Digest::SHA1, OpenSSL::Digest::SHA512] + digest_algorithms = [Digest::SHA1, Digest::SHA512] - digests = @tar_writer.add_file_digest 'x', 0644, digest_algorithms do |io| - io.write 'a' * 10 + Time.stub :now, Time.at(1458518157) do + digests = @tar_writer.add_file_digest 'x', 0644, digest_algorithms do |io| + io.write 'a' * 10 + end + + assert_equal '3495ff69d34671d1e15b33a63c1379fdedd3a32a', + digests['SHA1'].hexdigest + assert_equal '4714870aff6c97ca09d135834fdb58a6389a50c1' \ + '1fef8ec4afef466fb60a23ac6b7a9c92658f14df' \ + '4993d6b40a4e4d8424196afc347e97640d68de61' \ + 'e1cf14b0', + digests['SHA512'].hexdigest + + assert_headers_equal(tar_file_header('x', '', 0644, 10, Time.now), + @io.string[0, 512]) end - - assert_equal '3495ff69d34671d1e15b33a63c1379fdedd3a32a', - digests['SHA1'].hexdigest - assert_equal '4714870aff6c97ca09d135834fdb58a6389a50c1' \ - '1fef8ec4afef466fb60a23ac6b7a9c92658f14df' \ - '4993d6b40a4e4d8424196afc347e97640d68de61' \ - 'e1cf14b0', - digests['SHA512'].hexdigest - - assert_headers_equal(tar_file_header('x', '', 0644, 10), - @io.string[0, 512]) assert_equal "aaaaaaaaaa#{"\0" * 502}", @io.string[512, 512] assert_equal 1024, @io.pos end def test_add_file_signer + skip 'openssl is missing' unless defined?(OpenSSL::SSL) + signer = Gem::Security::Signer.new PRIVATE_KEY, [PUBLIC_CERT] - @tar_writer.add_file_signed 'x', 0644, signer do |io| - io.write 'a' * 10 + Time.stub :now, Time.at(1458518157) do + @tar_writer.add_file_signed 'x', 0644, signer do |io| + io.write 'a' * 10 + end + + assert_headers_equal(tar_file_header('x', '', 0644, 10, Time.now), + @io.string[0, 512]) + + + assert_equal "aaaaaaaaaa#{"\0" * 502}", @io.string[512, 512] + + digest = signer.digest_algorithm.new + digest.update 'a' * 10 + + signature = signer.sign digest.digest + + assert_headers_equal(tar_file_header('x.sig', '', 0444, signature.length, + Time.now), + @io.string[1024, 512]) + assert_equal "#{signature}#{"\0" * (512 - signature.length)}", + @io.string[1536, 512] + + assert_equal 2048, @io.pos end - assert_headers_equal(tar_file_header('x', '', 0644, 10), - @io.string[0, 512]) - assert_equal "aaaaaaaaaa#{"\0" * 502}", @io.string[512, 512] - - digest = signer.digest_algorithm.new - digest.update 'a' * 10 - - signature = signer.sign digest.digest - - assert_headers_equal(tar_file_header('x.sig', '', 0444, signature.length), - @io.string[1024, 512]) - assert_equal "#{signature}#{"\0" * (512 - signature.length)}", - @io.string[1536, 512] - - assert_equal 2048, @io.pos end def test_add_file_signer_empty signer = Gem::Security::Signer.new nil, nil - @tar_writer.add_file_signed 'x', 0644, signer do |io| - io.write 'a' * 10 - end + Time.stub :now, Time.at(1458518157) do - assert_headers_equal(tar_file_header('x', '', 0644, 10), + @tar_writer.add_file_signed 'x', 0644, signer do |io| + io.write 'a' * 10 + end + + assert_headers_equal(tar_file_header('x', '', 0644, 10, Time.now), @io.string[0, 512]) + end assert_equal "aaaaaaaaaa#{"\0" * 502}", @io.string[512, 512] - digest = signer.digest_algorithm.new - digest.update 'a' * 10 - assert_equal 1024, @io.pos end def test_add_file_simple - @tar_writer.add_file_simple 'x', 0644, 10 do |io| io.write "a" * 10 end + Time.stub :now, Time.at(1458518157) do + @tar_writer.add_file_simple 'x', 0644, 10 do |io| io.write "a" * 10 end - assert_headers_equal(tar_file_header('x', '', 0644, 10), + assert_headers_equal(tar_file_header('x', '', 0644, 10, Time.now), @io.string[0, 512]) + end assert_equal "aaaaaaaaaa#{"\0" * 502}", @io.string[512, 512] assert_equal 1024, @io.pos end def test_add_file_simple_padding - @tar_writer.add_file_simple 'x', 0, 100 + Time.stub :now, Time.at(1458518157) do + @tar_writer.add_file_simple 'x', 0, 100 - assert_headers_equal tar_file_header('x', '', 0, 100), + assert_headers_equal tar_file_header('x', '', 0, 100, Time.now), @io.string[0, 512] + end assert_equal "\0" * 512, @io.string[512, 512] end @@ -182,11 +201,14 @@ class TestGemPackageTarWriter < Gem::Package::TarTestCase end def test_mkdir - @tar_writer.mkdir 'foo', 0644 + Time.stub :now, Time.at(1458518157) do + @tar_writer.mkdir 'foo', 0644 - assert_headers_equal tar_dir_header('foo', '', 0644), - @io.string[0, 512] - assert_equal 512, @io.pos + assert_headers_equal tar_dir_header('foo', '', 0644, Time.now), + @io.string[0, 512] + + assert_equal 512, @io.pos + end end def test_split_name diff --git a/test/rubygems/test_gem_path_support.rb b/test/rubygems/test_gem_path_support.rb index bffc66f7dc..879cc98b5f 100644 --- a/test/rubygems/test_gem_path_support.rb +++ b/test/rubygems/test_gem_path_support.rb @@ -64,4 +64,21 @@ class TestGemPathSupport < Gem::TestCase def util_path ENV["GEM_PATH"].split(File::PATH_SEPARATOR) end + + def test_initialize_spec + ENV["GEM_SPEC_CACHE"] = nil + + ps = Gem::PathSupport.new + assert_equal Gem.default_spec_cache_dir, ps.spec_cache_dir + + ENV["GEM_SPEC_CACHE"] = 'bar' + + ps = Gem::PathSupport.new + assert_equal ENV["GEM_SPEC_CACHE"], ps.spec_cache_dir + + ENV["GEM_SPEC_CACHE"] = File.join @tempdir, 'spec_cache' + + ps = Gem::PathSupport.new "GEM_SPEC_CACHE" => "foo" + assert_equal "foo", ps.spec_cache_dir + end end diff --git a/test/rubygems/test_gem_platform.rb b/test/rubygems/test_gem_platform.rb index 1112a013f5..5966710dad 100644 --- a/test/rubygems/test_gem_platform.rb +++ b/test/rubygems/test_gem_platform.rb @@ -186,6 +186,24 @@ class TestGemPlatform < Gem::TestCase assert((x86_darwin8 === Gem::Platform.local), 'universal =~ x86') end + def test_equals3_cpu_arm + arm = Gem::Platform.new 'arm-linux' + armv5 = Gem::Platform.new 'armv5-linux' + armv7 = Gem::Platform.new 'armv7-linux' + + util_set_arch 'armv5-linux' + assert((arm === Gem::Platform.local), 'arm === armv5') + assert((armv5 === Gem::Platform.local), 'armv5 === armv5') + refute((armv7 === Gem::Platform.local), 'armv7 === armv5') + refute((Gem::Platform.local === arm), 'armv5 === arm') + + util_set_arch 'armv7-linux' + assert((arm === Gem::Platform.local), 'arm === armv7') + refute((armv5 === Gem::Platform.local), 'armv5 === armv7') + assert((armv7 === Gem::Platform.local), 'armv7 === armv7') + refute((Gem::Platform.local === arm), 'armv7 === arm') + end + def test_equals3_version util_set_arch 'i686-darwin8' diff --git a/test/rubygems/test_gem_remote_fetcher.rb b/test/rubygems/test_gem_remote_fetcher.rb index d3cc388db4..7ffb638436 100644 --- a/test/rubygems/test_gem_remote_fetcher.rb +++ b/test/rubygems/test_gem_remote_fetcher.rb @@ -1,7 +1,13 @@ require 'rubygems/test_case' -require 'ostruct' + require 'webrick' -require 'webrick/https' +begin + require 'webrick/https' +rescue LoadError => e + raise unless (e.respond_to?(:path) && e.path == 'openssl') || + e.message =~ / -- openssl$/ +end + require 'rubygems/remote_fetcher' require 'rubygems/package' require 'minitest/mock' @@ -128,19 +134,7 @@ gems: refute_nil fetcher assert_kind_of Gem::RemoteFetcher, fetcher - assert_equal proxy_uri, fetcher.instance_variable_get(:@proxy_uri).to_s - end - - def test_self_fetcher_with_proxy_URI - proxy_uri = URI.parse 'http://proxy.example.com' - Gem.configuration[:http_proxy] = proxy_uri - Gem::RemoteFetcher.fetcher = nil - - fetcher = Gem::RemoteFetcher.fetcher - refute_nil fetcher - - assert_kind_of Gem::RemoteFetcher, fetcher - assert_equal proxy_uri, fetcher.instance_variable_get(:@proxy_uri) + assert_equal proxy_uri, fetcher.instance_variable_get(:@proxy).to_s end def test_fetch_size_bad_uri @@ -155,7 +149,7 @@ gems: def test_fetch_size_socket_error fetcher = Gem::RemoteFetcher.new nil - def fetcher.connection_for(uri) + def fetcher.request(uri, request_class, last_modified = nil) raise SocketError, "tarded" end @@ -414,70 +408,6 @@ gems: assert_equal @a2.file_name, File.basename(gem) end - def test_explicit_proxy - use_ui @ui do - fetcher = Gem::RemoteFetcher.new @proxy_uri - assert_equal PROXY_DATA.size, fetcher.fetch_size(@server_uri) - assert_data_from_proxy fetcher.fetch_path(@server_uri) - end - end - - def test_explicit_proxy_with_user_auth - use_ui @ui do - uri = URI.parse @proxy_uri - uri.user, uri.password = 'foo', 'bar' - fetcher = Gem::RemoteFetcher.new uri.to_s - proxy = fetcher.instance_variable_get("@proxy_uri") - assert_equal 'foo', proxy.user - assert_equal 'bar', proxy.password - assert_data_from_proxy fetcher.fetch_path(@server_uri) - end - - use_ui @ui do - uri = URI.parse @proxy_uri - uri.user, uri.password = 'domain%5Cuser', 'bar' - fetcher = Gem::RemoteFetcher.new uri.to_s - proxy = fetcher.instance_variable_get("@proxy_uri") - assert_equal 'domain\user', fetcher.unescape(proxy.user) - assert_equal 'bar', proxy.password - assert_data_from_proxy fetcher.fetch_path(@server_uri) - end - - use_ui @ui do - uri = URI.parse @proxy_uri - uri.user, uri.password = 'user', 'my%20pass' - fetcher = Gem::RemoteFetcher.new uri.to_s - proxy = fetcher.instance_variable_get("@proxy_uri") - assert_equal 'user', proxy.user - assert_equal 'my pass', fetcher.unescape(proxy.password) - assert_data_from_proxy fetcher.fetch_path(@server_uri) - end - end - - def test_explicit_proxy_with_user_auth_in_env - use_ui @ui do - ENV['http_proxy'] = @proxy_uri - ENV['http_proxy_user'] = 'foo' - ENV['http_proxy_pass'] = 'bar' - fetcher = Gem::RemoteFetcher.new nil - proxy = fetcher.instance_variable_get("@proxy_uri") - assert_equal 'foo', proxy.user - assert_equal 'bar', proxy.password - assert_data_from_proxy fetcher.fetch_path(@server_uri) - end - - use_ui @ui do - ENV['http_proxy'] = @proxy_uri - ENV['http_proxy_user'] = 'foo\user' - ENV['http_proxy_pass'] = 'my bar' - fetcher = Gem::RemoteFetcher.new nil - proxy = fetcher.instance_variable_get("@proxy_uri") - assert_equal 'foo\user', fetcher.unescape(proxy.user) - assert_equal 'my bar', fetcher.unescape(proxy.password) - assert_data_from_proxy fetcher.fetch_path(@server_uri) - end - end - def test_fetch_path_gzip fetcher = Gem::RemoteFetcher.new nil @@ -560,22 +490,6 @@ gems: assert_equal nil, fetcher.fetch_path(URI.parse(@gem_repo), Time.at(0)) end - def test_get_proxy_from_env_auto_normalizes - fetcher = Gem::RemoteFetcher.new(nil) - ENV['HTTP_PROXY'] = 'fakeurl:12345' - - assert_equal('http://fakeurl:12345', fetcher.get_proxy_from_env.to_s) - end - - def test_get_proxy_from_env_empty - ENV['HTTP_PROXY'] = '' - ENV.delete 'http_proxy' - - fetcher = Gem::RemoteFetcher.new nil - - assert_equal nil, fetcher.send(:get_proxy_from_env) - end - def test_implicit_no_proxy use_ui @ui do ENV['http_proxy'] = 'http://fakeurl:12345' @@ -611,9 +525,7 @@ gems: fetcher = Gem::RemoteFetcher.new nil url = 'http://gems.example.com/redirect' - conn = Object.new - def conn.started?() true end - def conn.request(req) + def fetcher.request(uri, request_class, last_modified = nil) url = 'http://gems.example.com/redirect' unless defined? @requested then @requested = true @@ -627,9 +539,6 @@ gems: end end - conn = { "#{Thread.current.object_id}:gems.example.com:80" => conn } - fetcher.instance_variable_set :@connections, conn - data = fetcher.fetch_http URI.parse(url) assert_equal 'real_path', data @@ -639,18 +548,13 @@ gems: fetcher = Gem::RemoteFetcher.new nil url = 'http://gems.example.com/redirect' - conn = Object.new - def conn.started?() true end - def conn.request(req) + def fetcher.request(uri, request_class, last_modified = nil) url = 'http://gems.example.com/redirect' res = Net::HTTPMovedPermanently.new nil, 301, nil res.add_field 'Location', url res end - conn = { "#{Thread.current.object_id}:gems.example.com:80" => conn } - fetcher.instance_variable_set :@connections, conn - e = assert_raises Gem::RemoteFetcher::FetchError do fetcher.fetch_http URI.parse(url) end @@ -658,14 +562,6 @@ gems: assert_equal "too many redirects (#{url})", e.message end - def test_normalize_uri - assert_equal 'FILE://example/', @fetcher.normalize_uri('FILE://example/') - assert_equal 'FTP://example/', @fetcher.normalize_uri('FTP://example/') - assert_equal 'HTTP://example/', @fetcher.normalize_uri('HTTP://example/') - assert_equal 'HTTPS://example/', @fetcher.normalize_uri('HTTPS://example/') - assert_equal 'http://example/', @fetcher.normalize_uri('example/') - end - def test_observe_no_proxy_env_single_host use_ui @ui do ENV["http_proxy"] = @proxy_uri @@ -684,117 +580,6 @@ gems: end end - def test_request - uri = URI.parse "#{@gem_repo}/specs.#{Gem.marshal_version}" - util_stub_connection_for :body => :junk, :code => 200 - - response = @fetcher.request uri, Net::HTTP::Get - - assert_equal 200, response.code - assert_equal :junk, response.body - end - - def test_request_head - uri = URI.parse "#{@gem_repo}/specs.#{Gem.marshal_version}" - util_stub_connection_for :body => '', :code => 200 - response = @fetcher.request uri, Net::HTTP::Head - - assert_equal 200, response.code - assert_equal '', response.body - end - - def test_request_unmodified - uri = URI.parse "#{@gem_repo}/specs.#{Gem.marshal_version}" - conn = util_stub_connection_for :body => '', :code => 304 - - t = Time.now - response = @fetcher.request uri, Net::HTTP::Head, t - - assert_equal 304, response.code - assert_equal '', response.body - - assert_equal t.rfc2822, conn.payload['if-modified-since'] - end - - def test_user_agent - ua = @fetcher.user_agent - - assert_match %r%^RubyGems/\S+ \S+ Ruby/\S+ \(.*?\)%, ua - assert_match %r%RubyGems/#{Regexp.escape Gem::VERSION}%, ua - assert_match %r% #{Regexp.escape Gem::Platform.local.to_s} %, ua - assert_match %r%Ruby/#{Regexp.escape RUBY_VERSION}%, ua - assert_match %r%\(#{Regexp.escape RUBY_RELEASE_DATE} %, ua - end - - def test_user_agent_engine - util_save_version - - Object.send :remove_const, :RUBY_ENGINE if defined?(RUBY_ENGINE) - Object.send :const_set, :RUBY_ENGINE, 'vroom' - - ua = @fetcher.user_agent - - assert_match %r%\) vroom%, ua - ensure - util_restore_version - end - - def test_user_agent_engine_ruby - util_save_version - - Object.send :remove_const, :RUBY_ENGINE if defined?(RUBY_ENGINE) - Object.send :const_set, :RUBY_ENGINE, 'ruby' - - ua = @fetcher.user_agent - - assert_match %r%\)%, ua - ensure - util_restore_version - end - - def test_user_agent_patchlevel - util_save_version - - Object.send :remove_const, :RUBY_PATCHLEVEL - Object.send :const_set, :RUBY_PATCHLEVEL, 5 - - ua = @fetcher.user_agent - - assert_match %r% patchlevel 5\)%, ua - ensure - util_restore_version - end - - def test_user_agent_revision - util_save_version - - Object.send :remove_const, :RUBY_PATCHLEVEL - Object.send :const_set, :RUBY_PATCHLEVEL, -1 - Object.send :remove_const, :RUBY_REVISION if defined?(RUBY_REVISION) - Object.send :const_set, :RUBY_REVISION, 6 - - ua = @fetcher.user_agent - - assert_match %r% revision 6\)%, ua - assert_match %r%Ruby/#{Regexp.escape RUBY_VERSION}dev%, ua - ensure - util_restore_version - end - - def test_user_agent_revision_missing - util_save_version - - Object.send :remove_const, :RUBY_PATCHLEVEL - Object.send :const_set, :RUBY_PATCHLEVEL, -1 - Object.send :remove_const, :RUBY_REVISION if defined?(RUBY_REVISION) - - ua = @fetcher.user_agent - - assert_match %r%\(#{Regexp.escape RUBY_RELEASE_DATE}\)%, ua - ensure - util_restore_version - end - def test_yaml_error_on_size use_ui @ui do self.class.enable_yaml = false @@ -811,6 +596,42 @@ gems: end end + def test_ssl_client_cert_auth_connection + skip 'openssl is missing' unless defined?(OpenSSL::SSL) + + ssl_server = self.class.start_ssl_server({ + :SSLVerifyClient => + OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT}) + + temp_ca_cert = File.join(DIR, 'ca_cert.pem') + temp_client_cert = File.join(DIR, 'client.pem') + + with_configured_fetcher( + ":ssl_ca_cert: #{temp_ca_cert}\n" + + ":ssl_client_cert: #{temp_client_cert}\n") do |fetcher| + fetcher.fetch_path("https://localhost:#{ssl_server.config[:Port]}/yaml") + end + end + + def test_do_not_allow_invalid_client_cert_auth_connection + skip 'openssl is missing' unless defined?(OpenSSL::SSL) + + ssl_server = self.class.start_ssl_server({ + :SSLVerifyClient => + OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT}) + + temp_ca_cert = File.join(DIR, 'ca_cert.pem') + temp_client_cert = File.join(DIR, 'invalid_client.pem') + + with_configured_fetcher( + ":ssl_ca_cert: #{temp_ca_cert}\n" + + ":ssl_client_cert: #{temp_client_cert}\n") do |fetcher| + assert_raises Gem::RemoteFetcher::FetchError do + fetcher.fetch_path("https://localhost:#{ssl_server.config[:Port]}/yaml") + end + end + end + def test_do_not_allow_insecure_ssl_connection_by_default ssl_server = self.class.start_ssl_server with_configured_fetcher do |fetcher| @@ -850,18 +671,6 @@ gems: Gem.configuration = nil end - def util_stub_connection_for hash - def @fetcher.connection= conn - @conn = conn - end - - def @fetcher.connection_for uri - @conn - end - - @fetcher.connection = Conn.new OpenStruct.new(hash) - end - def assert_error(exception_class=Exception) got_exception = false @@ -882,20 +691,6 @@ gems: assert_match(/0\.4\.2/, data, "Data is not from proxy") end - class Conn - attr_accessor :payload - - def initialize(response) - @response = response - self.payload = nil - end - - def request(req) - self.payload = req - @response - end - end - class NilLog < WEBrick::Log def log(level, data) #Do nothing end @@ -913,9 +708,11 @@ gems: end DIR = File.expand_path(File.dirname(__FILE__)) - DH_PARAM = OpenSSL::PKey::DH.new(128) def start_ssl_server(config = {}) + raise MiniTest::Skip, 'openssl not installed' unless + defined?(OpenSSL::SSL) + null_logger = NilLog.new server = WEBrick::HTTPServer.new({ :Port => 0, @@ -934,7 +731,7 @@ gems: server.mount_proc("/insecure_redirect") { |req, res| res.set_redirect(WEBrick::HTTPStatus::MovedPermanently, req.query['to']) } - server.ssl_context.tmp_dh_callback = proc { DH_PARAM } + server.ssl_context.tmp_dh_callback = proc { OpenSSL::PKey::DH.new 128 } t = Thread.new do begin server.start @@ -953,8 +750,6 @@ gems: server end - - private def start_server(port, data) @@ -1015,24 +810,5 @@ gems: assert_equal "/home/skillet", @fetcher.correct_for_windows_path(path) end - def util_save_version - @orig_RUBY_ENGINE = RUBY_ENGINE if defined? RUBY_ENGINE - @orig_RUBY_PATCHLEVEL = RUBY_PATCHLEVEL - @orig_RUBY_REVISION = RUBY_REVISION if defined? RUBY_REVISION - end - - def util_restore_version - Object.send :remove_const, :RUBY_ENGINE if defined?(RUBY_ENGINE) - Object.send :const_set, :RUBY_ENGINE, @orig_RUBY_ENGINE if - defined?(@orig_RUBY_ENGINE) - - Object.send :remove_const, :RUBY_PATCHLEVEL - Object.send :const_set, :RUBY_PATCHLEVEL, @orig_RUBY_PATCHLEVEL - - Object.send :remove_const, :RUBY_REVISION if defined?(RUBY_REVISION) - Object.send :const_set, :RUBY_REVISION, @orig_RUBY_REVISION if - defined?(@orig_RUBY_REVISION) - end - end diff --git a/test/rubygems/test_gem_request.rb b/test/rubygems/test_gem_request.rb new file mode 100644 index 0000000000..869c6730a9 --- /dev/null +++ b/test/rubygems/test_gem_request.rb @@ -0,0 +1,239 @@ +require 'rubygems/test_case' +require 'rubygems/request' +require 'ostruct' + +class TestGemRequest < Gem::TestCase + + def setup + @proxies = %w[http_proxy HTTP_PROXY http_proxy_user HTTP_PROXY_USER http_proxy_pass HTTP_PROXY_PASS no_proxy NO_PROXY] + @old_proxies = @proxies.map {|k| ENV[k] } + @proxies.each {|k| ENV[k] = nil } + + super + + @proxy_uri = "http://localhost:1234" + + @request = Gem::Request.new nil, nil, nil, nil + end + + def teardown + super + Gem.configuration[:http_proxy] = nil + @proxies.each_with_index {|k, i| ENV[k] = @old_proxies[i] } + end + + def test_initialize_proxy + proxy_uri = 'http://proxy.example.com' + + request = Gem::Request.new nil, nil, nil, proxy_uri + + assert_equal proxy_uri, request.proxy_uri.to_s + end + + def test_initialize_proxy_URI + proxy_uri = 'http://proxy.example.com' + + request = Gem::Request.new nil, nil, nil, URI(proxy_uri) + + assert_equal proxy_uri, request.proxy_uri.to_s + end + + def test_initialize_proxy_ENV + ENV['http_proxy'] = @proxy_uri + ENV['http_proxy_user'] = 'foo' + ENV['http_proxy_pass'] = 'bar' + + request = Gem::Request.new nil, nil, nil, nil + + proxy = request.proxy_uri + + assert_equal 'foo', proxy.user + assert_equal 'bar', proxy.password + end + + def test_get_proxy_from_env_domain + ENV['http_proxy'] = @proxy_uri + ENV['http_proxy_user'] = 'foo\user' + ENV['http_proxy_pass'] = 'my bar' + + proxy = @request.get_proxy_from_env + + assert_equal 'foo\user', Gem::UriFormatter.new(proxy.user).unescape + assert_equal 'my bar', Gem::UriFormatter.new(proxy.password).unescape + end + + def test_get_proxy_from_env_normalize + ENV['HTTP_PROXY'] = 'fakeurl:12345' + + assert_equal 'http://fakeurl:12345', @request.get_proxy_from_env.to_s + end + + def test_get_proxy_from_env_empty + ENV['HTTP_PROXY'] = '' + ENV.delete 'http_proxy' + + assert_nil @request.get_proxy_from_env + end + + def test_fetch + uri = URI.parse "#{@gem_repo}/specs.#{Gem.marshal_version}" + @request = Gem::Request.new(uri, Net::HTTP::Get, nil, nil) + util_stub_connection_for :body => :junk, :code => 200 + + response = @request.fetch + + assert_equal 200, response.code + assert_equal :junk, response.body + end + + def test_fetch_head + uri = URI.parse "#{@gem_repo}/specs.#{Gem.marshal_version}" + @request = Gem::Request.new(uri, Net::HTTP::Get, nil, nil) + util_stub_connection_for :body => '', :code => 200 + + response = @request.fetch + + assert_equal 200, response.code + assert_equal '', response.body + end + + def test_fetch_unmodified + uri = URI.parse "#{@gem_repo}/specs.#{Gem.marshal_version}" + t = Time.now + @request = Gem::Request.new(uri, Net::HTTP::Get, t, nil) + conn = util_stub_connection_for :body => '', :code => 304 + + response = @request.fetch + + assert_equal 304, response.code + assert_equal '', response.body + + assert_equal t.rfc2822, conn.payload['if-modified-since'] + end + + def test_user_agent + ua = Gem::Request.new(nil, nil, nil, nil).user_agent + + assert_match %r%^RubyGems/\S+ \S+ Ruby/\S+ \(.*?\)%, ua + assert_match %r%RubyGems/#{Regexp.escape Gem::VERSION}%, ua + assert_match %r% #{Regexp.escape Gem::Platform.local.to_s} %, ua + assert_match %r%Ruby/#{Regexp.escape RUBY_VERSION}%, ua + assert_match %r%\(#{Regexp.escape RUBY_RELEASE_DATE} %, ua + end + + def test_user_agent_engine + util_save_version + + Object.send :remove_const, :RUBY_ENGINE if defined?(RUBY_ENGINE) + Object.send :const_set, :RUBY_ENGINE, 'vroom' + + ua = Gem::Request.new(nil, nil, nil, nil).user_agent + + assert_match %r%\) vroom%, ua + ensure + util_restore_version + end + + def test_user_agent_engine_ruby + util_save_version + + Object.send :remove_const, :RUBY_ENGINE if defined?(RUBY_ENGINE) + Object.send :const_set, :RUBY_ENGINE, 'ruby' + + ua = Gem::Request.new(nil, nil, nil, nil).user_agent + + assert_match %r%\)%, ua + ensure + util_restore_version + end + + def test_user_agent_patchlevel + util_save_version + + Object.send :remove_const, :RUBY_PATCHLEVEL + Object.send :const_set, :RUBY_PATCHLEVEL, 5 + + ua = Gem::Request.new(nil, nil, nil, nil).user_agent + + assert_match %r% patchlevel 5\)%, ua + ensure + util_restore_version + end + + def test_user_agent_revision + util_save_version + + Object.send :remove_const, :RUBY_PATCHLEVEL + Object.send :const_set, :RUBY_PATCHLEVEL, -1 + Object.send :remove_const, :RUBY_REVISION if defined?(RUBY_REVISION) + Object.send :const_set, :RUBY_REVISION, 6 + + ua = Gem::Request.new(nil, nil, nil, nil).user_agent + + assert_match %r% revision 6\)%, ua + assert_match %r%Ruby/#{Regexp.escape RUBY_VERSION}dev%, ua + ensure + util_restore_version + end + + def test_user_agent_revision_missing + util_save_version + + Object.send :remove_const, :RUBY_PATCHLEVEL + Object.send :const_set, :RUBY_PATCHLEVEL, -1 + Object.send :remove_const, :RUBY_REVISION if defined?(RUBY_REVISION) + + ua = Gem::Request.new(nil, nil, nil, nil).user_agent + + assert_match %r%\(#{Regexp.escape RUBY_RELEASE_DATE}\)%, ua + ensure + util_restore_version + end + + def util_restore_version + Object.send :remove_const, :RUBY_ENGINE if defined?(RUBY_ENGINE) + Object.send :const_set, :RUBY_ENGINE, @orig_RUBY_ENGINE if + defined?(@orig_RUBY_ENGINE) + + Object.send :remove_const, :RUBY_PATCHLEVEL + Object.send :const_set, :RUBY_PATCHLEVEL, @orig_RUBY_PATCHLEVEL + + Object.send :remove_const, :RUBY_REVISION if defined?(RUBY_REVISION) + Object.send :const_set, :RUBY_REVISION, @orig_RUBY_REVISION if + defined?(@orig_RUBY_REVISION) + end + + def util_save_version + @orig_RUBY_ENGINE = RUBY_ENGINE if defined? RUBY_ENGINE + @orig_RUBY_PATCHLEVEL = RUBY_PATCHLEVEL + @orig_RUBY_REVISION = RUBY_REVISION if defined? RUBY_REVISION + end + + def util_stub_connection_for hash + def @request.connection= conn + @conn = conn + end + + def @request.connection_for uri + @conn + end + + @request.connection = Conn.new OpenStruct.new(hash) + end + + class Conn + attr_accessor :payload + + def initialize(response) + @response = response + self.payload = nil + end + + def request(req) + self.payload = req + @response + end + end + +end + diff --git a/test/rubygems/test_gem_security.rb b/test/rubygems/test_gem_security.rb index 737881d7bc..2e6a56fc8f 100644 --- a/test/rubygems/test_gem_security.rb +++ b/test/rubygems/test_gem_security.rb @@ -2,6 +2,10 @@ require 'rubygems/test_case' require 'rubygems/security' require 'rubygems/fix_openssl_warnings' if RUBY_VERSION < "1.9" +unless defined?(OpenSSL::SSL) then + warn 'Skipping Gem::Security tests. openssl not found.' +end + class TestGemSecurity < Gem::TestCase CHILD_KEY = load_key 'child' @@ -246,5 +250,57 @@ class TestGemSecurity < Gem::TestCase assert_equal expected, trust_dir.dir end -end + def test_class_write + key = @SEC.create_key 256 + + path = File.join @tempdir, 'test-private_key.pem' + + @SEC.write key, path + + assert_path_exists path + + key_from_file = File.read path + + assert_equal key.to_pem, key_from_file + end + + def test_class_write_encrypted + key = @SEC.create_key 256 + + path = File.join @tempdir, 'test-private_encrypted_key.pem' + + passphrase = 'It should be long.' + + @SEC.write key, path, 0600, passphrase + + assert_path_exists path + + key_from_file = OpenSSL::PKey::RSA.new File.read(path), passphrase + + assert_equal key.to_pem, key_from_file.to_pem + end + + def test_class_write_encrypted_cipher + key = @SEC.create_key 256 + + path = File.join @tempdir, 'test-private_encrypted__with_non_default_cipher_key.pem' + + passphrase = 'It should be long.' + + cipher = OpenSSL::Cipher.new('aes192') + + @SEC.write key, path, 0600, passphrase, cipher + + assert_path_exists path + + key_file_contents = File.read(path) + + assert key_file_contents.split("\n")[2].match(cipher.name) + + key_from_file = OpenSSL::PKey::RSA.new key_file_contents, passphrase + + assert_equal key.to_pem, key_from_file.to_pem + end + +end if defined?(OpenSSL::SSL) diff --git a/test/rubygems/test_gem_security_policy.rb b/test/rubygems/test_gem_security_policy.rb index 1ce93fbd95..5b960c4d48 100644 --- a/test/rubygems/test_gem_security_policy.rb +++ b/test/rubygems/test_gem_security_policy.rb @@ -2,6 +2,10 @@ require 'rubygems/test_case' +unless defined?(OpenSSL::SSL) then + warn 'Skipping Gem::Security::Policy tests. openssl not found.' +end + class TestGemSecurityPolicy < Gem::TestCase ALTERNATE_KEY = load_key 'alternate' @@ -11,6 +15,7 @@ class TestGemSecurityPolicy < Gem::TestCase INVALIDCHILD_KEY = load_key 'invalidchild' ALTERNATE_CERT = load_cert 'alternate' + CA_CERT = load_cert 'ca' CHILD_CERT = load_cert 'child' EXPIRED_CERT = load_cert 'expired' FUTURE_CERT = load_cert 'future' @@ -285,6 +290,11 @@ class TestGemSecurityPolicy < Gem::TestCase "(root of signing cert #{CHILD_CERT.subject})", e.message end + def test_subject + assert_equal 'email:nobody@example', @no.subject(PUBLIC_CERT) + assert_equal '/C=JP/O=JIN.GR.JP/OU=RRR/CN=CA', @no.subject(CA_CERT) + end + def test_verify Gem::Security.trust_dir.trust_cert PUBLIC_CERT @@ -325,6 +335,22 @@ class TestGemSecurityPolicy < Gem::TestCase assert_equal 'missing digest for 0', e.message end + def test_verify_no_signatures + Gem::Security.trust_dir.trust_cert PUBLIC_CERT + + digests, = dummy_signatures + + use_ui @ui do + @no.verify [PUBLIC_CERT], nil, digests, {}, 'some_gem' + end + + assert_match "WARNING: some_gem is not signed\n", @ui.error + + assert_raises Gem::Security::Exception do + @almost_no.verify [PUBLIC_CERT], nil, digests, {} + end + end + def test_verify_not_enough_signatures Gem::Security.trust_dir.trust_cert PUBLIC_CERT @@ -341,6 +367,21 @@ class TestGemSecurityPolicy < Gem::TestCase assert_equal 'missing digest for 1', e.message end + def test_verify_no_trust + digests, signatures = dummy_signatures + + use_ui @ui do + @low.verify [PUBLIC_CERT], nil, digests, signatures, 'some_gem' + end + + assert_equal "WARNING: email:nobody@example is not trusted for some_gem\n", + @ui.error + + assert_raises Gem::Security::Exception do + @medium.verify [PUBLIC_CERT], nil, digests, signatures + end + end + def test_verify_wrong_digest_type Gem::Security.trust_dir.trust_cert PUBLIC_CERT @@ -484,5 +525,5 @@ class TestGemSecurityPolicy < Gem::TestCase return digests, signatures end -end +end if defined?(OpenSSL::SSL) diff --git a/test/rubygems/test_gem_security_signer.rb b/test/rubygems/test_gem_security_signer.rb index 59c5089fec..c984382947 100644 --- a/test/rubygems/test_gem_security_signer.rb +++ b/test/rubygems/test_gem_security_signer.rb @@ -1,5 +1,9 @@ require 'rubygems/test_case' +unless defined?(OpenSSL::SSL) then + warn 'Skipping Gem::Security::Signer tests. openssl not found.' +end + class TestGemSecuritySigner < Gem::TestCase ALTERNATE_KEY = load_key 'alternate' @@ -72,6 +76,14 @@ class TestGemSecuritySigner < Gem::TestCase assert_equal PRIVATE_KEY.to_s, signer.key.to_s end + def test_initialize_encrypted_key_path + key_file = ENCRYPTED_PRIVATE_KEY_PATH + + signer = Gem::Security::Signer.new key_file, nil, PRIVATE_KEY_PASSPHRASE + + assert_equal ENCRYPTED_PRIVATE_KEY.to_s, signer.key.to_s + end + def test_load_cert_chain Gem::Security.trust_dir.trust_cert PUBLIC_CERT @@ -186,5 +198,5 @@ c7NM7KZZjj7G++SXjYTEI1PHSA7aFQ/i/+qSUvx+Pg== end end -end +end if defined?(OpenSSL::SSL) diff --git a/test/rubygems/test_gem_security_trust_dir.rb b/test/rubygems/test_gem_security_trust_dir.rb index 56c2feb44c..7b0d450bd6 100644 --- a/test/rubygems/test_gem_security_trust_dir.rb +++ b/test/rubygems/test_gem_security_trust_dir.rb @@ -1,5 +1,9 @@ require 'rubygems/test_case' +unless defined?(OpenSSL::SSL) then + warn 'Skipping Gem::Security::TrustDir tests. openssl not found.' +end + class TestGemSecurityTrustDir < Gem::TestCase CHILD_CERT = load_cert 'child' @@ -90,5 +94,5 @@ class TestGemSecurityTrustDir < Gem::TestCase assert_equal mask, File.stat(@dest_dir).mode unless win_platform? end -end +end if defined?(OpenSSL::SSL) diff --git a/test/rubygems/test_gem_source.rb b/test/rubygems/test_gem_source.rb index 2629f180a9..1303978a8e 100644 --- a/test/rubygems/test_gem_source.rb +++ b/test/rubygems/test_gem_source.rb @@ -63,7 +63,7 @@ class TestGemSource < Gem::TestCase def test_cache_dir_escapes_windows_paths uri = URI.parse("file:///C:/WINDOWS/Temp/gem_repo") - root = File.join Gem.user_home, '.gem', 'specs' + root = Gem.spec_cache_dir cache_dir = @source.cache_dir(uri).gsub(root, '') assert cache_dir !~ /:/, "#{cache_dir} should not contain a :" end @@ -123,7 +123,7 @@ class TestGemSource < Gem::TestCase expected = @released assert_equal expected, @source.load_specs(:released) - cache_dir = File.join Gem.user_home, '.gem', 'specs', 'gems.example.com%80' + cache_dir = File.join Gem.spec_cache_dir, 'gems.example.com%80' assert File.exist?(cache_dir), "#{cache_dir} does not exist" cache_file = File.join cache_dir, "specs.#{Gem.marshal_version}" @@ -138,7 +138,7 @@ class TestGemSource < Gem::TestCase @fetcher.data["#{@gem_repo}latest_specs.#{Gem.marshal_version}"] = ' ' * Marshal.dump(@latest_specs).length - cache_dir = File.join Gem.user_home, '.gem', 'specs', 'gems.example.com%80' + cache_dir = File.join Gem.spec_cache_dir, 'gems.example.com%80' FileUtils.mkdir_p cache_dir @@ -160,7 +160,7 @@ class TestGemSource < Gem::TestCase @fetcher.data["#{@gem_repo}latest_specs.#{Gem.marshal_version}.gz"] = util_gzip(Marshal.dump(@latest_specs)) - cache_dir = File.join Gem.user_home, '.gem', 'specs', 'gems.example.com%80' + cache_dir = File.join Gem.spec_cache_dir, 'gems.example.com%80' FileUtils.mkdir_p cache_dir diff --git a/test/rubygems/test_gem_source_local.rb b/test/rubygems/test_gem_source_local.rb index 54ce3d51be..8e901cfd21 100644 --- a/test/rubygems/test_gem_source_local.rb +++ b/test/rubygems/test_gem_source_local.rb @@ -1,5 +1,5 @@ require 'rubygems/test_case' -require 'rubygems/source_local' +require 'rubygems/source/local' require 'fileutils' @@ -76,8 +76,8 @@ class TestGemSourceLocal < Gem::TestCase uri = URI.parse "http://gems.example/foo" s = Gem::Source.new uri - assert_equal(-1, (@sl <=> s)) - assert_equal 1, (s <=> @sl) - assert_equal 0, (@sl <=> @sl) + assert_equal(-1, s <=> @sl) + assert_equal 0, @sl <=> @sl + assert_equal 1, @sl <=> s end end diff --git a/test/rubygems/test_gem_source_specific_file.rb b/test/rubygems/test_gem_source_specific_file.rb index 7ffcf482dc..1d8351781f 100644 --- a/test/rubygems/test_gem_source_specific_file.rb +++ b/test/rubygems/test_gem_source_specific_file.rb @@ -1,5 +1,5 @@ require 'rubygems/test_case' -require 'rubygems/source_specific_file' +require 'rubygems/source/specific_file' class TestGemSourceSpecificFile < Gem::TestCase def setup diff --git a/test/rubygems/test_gem_specification.rb b/test/rubygems/test_gem_specification.rb index 9b2ae82fe3..556115bdc6 100644 --- a/test/rubygems/test_gem_specification.rb +++ b/test/rubygems/test_gem_specification.rb @@ -38,10 +38,8 @@ Gem::Specification.new do |s| end EOF - def setup - super - - @a1 = quick_spec 'a', '1' do |s| + def make_spec_c1 + @c1 = quick_spec 'a', '1' do |s| s.executable = 'exec' s.extensions << 'ext/a/extconf.rb' s.test_file = 'test/suite.rb' @@ -56,6 +54,22 @@ end s.mark_version s.files = %w[lib/code.rb] end + end + + def setup + super + + @a1 = quick_spec 'a', '1' do |s| + s.executable = 'exec' + s.extensions << 'ext/a/extconf.rb' + s.test_file = 'test/suite.rb' + s.requirements << 'A working computer' + s.rubyforge_project = 'example' + s.license = 'MIT' + + s.mark_version + s.files = %w[lib/code.rb] + end @a2 = quick_spec 'a', '2' do |s| s.files = %w[lib/code.rb] @@ -66,6 +80,371 @@ end load 'rubygems/syck_hack.rb' end + def test_self_activate + foo = util_spec 'foo', '1' + + assert_activate %w[foo-1], foo + end + + def test_self_activate_ambiguous_direct + save_loaded_features do + a1 = new_spec "a", "1", "b" => "> 0" + b1 = new_spec("b", "1", { "c" => ">= 1" }, "lib/d.rb") + b2 = new_spec("b", "2", { "c" => ">= 2" }, "lib/d.rb") + c1 = new_spec "c", "1" + c2 = new_spec "c", "2" + + Gem::Specification.reset + install_specs a1, b1, b2, c1, c2 + + a1.activate + assert_equal %w(a-1), loaded_spec_names + assert_equal ["b (> 0)"], unresolved_names + + require "d" + + assert_equal %w(a-1 b-2 c-2), loaded_spec_names + assert_equal [], unresolved_names + end + end + + def test_self_activate_ambiguous_indirect + save_loaded_features do + a1 = new_spec "a", "1", "b" => "> 0" + b1 = new_spec "b", "1", "c" => ">= 1" + b2 = new_spec "b", "2", "c" => ">= 2" + c1 = new_spec "c", "1", nil, "lib/d.rb" + c2 = new_spec "c", "2", nil, "lib/d.rb" + + install_specs a1, b1, b2, c1, c2 + + a1.activate + assert_equal %w(a-1), loaded_spec_names + assert_equal ["b (> 0)"], unresolved_names + + require "d" + + assert_equal %w(a-1 b-2 c-2), loaded_spec_names + assert_equal [], unresolved_names + end + end + + def test_self_activate_ambiguous_indirect_conflict + save_loaded_features do + a1 = new_spec "a", "1", "b" => "> 0" + a2 = new_spec "a", "2", "b" => "> 0" + b1 = new_spec "b", "1", "c" => ">= 1" + b2 = new_spec "b", "2", "c" => ">= 2" + c1 = new_spec "c", "1", nil, "lib/d.rb" + c2 = new_spec("c", "2", { "a" => "1" }, "lib/d.rb") # conflicts with a-2 + + install_specs a1, a2, b1, b2, c1, c2 + + a2.activate + assert_equal %w(a-2), loaded_spec_names + assert_equal ["b (> 0)"], unresolved_names + + require "d" + + assert_equal %w(a-2 b-1 c-1), loaded_spec_names + assert_equal [], unresolved_names + end + end + + def test_self_activate_ambiguous_unrelated + save_loaded_features do + a1 = new_spec "a", "1", "b" => "> 0" + b1 = new_spec "b", "1", "c" => ">= 1" + b2 = new_spec "b", "2", "c" => ">= 2" + c1 = new_spec "c", "1" + c2 = new_spec "c", "2" + d1 = new_spec "d", "1", nil, "lib/d.rb" + + install_specs a1, b1, b2, c1, c2, d1 + + a1.activate + assert_equal %w(a-1), loaded_spec_names + assert_equal ["b (> 0)"], unresolved_names + + require "d" + + assert_equal %w(a-1 d-1), loaded_spec_names + assert_equal ["b (> 0)"], unresolved_names + end + end + + ## + # [A] depends on + # [C] = 1.0 depends on + # [B] = 2.0 + # [B] ~> 1.0 (satisfied by 1.0) + + def test_self_activate_checks_dependencies + a, _ = util_spec 'a', '1.0' + a.add_dependency 'c', '= 1.0' + a.add_dependency 'b', '~> 1.0' + + util_spec 'b', '1.0' + util_spec 'b', '2.0' + c, _ = util_spec 'c', '1.0', 'b' => '= 2.0' + + e = assert_raises Gem::LoadError do + assert_activate nil, a, c, "b" + end + + expected = "can't satisfy 'b (~> 1.0)', already activated 'b-2.0'" + assert_equal expected, e.message + end + + ## + # [A] depends on + # [B] ~> 1.0 (satisfied by 1.0) + # [C] = 1.0 depends on + # [B] = 2.0 + + def test_self_activate_divergent + a, _ = util_spec 'a', '1.0', 'b' => '~> 1.0', 'c' => '= 1.0' + util_spec 'b', '1.0' + util_spec 'b', '2.0' + c, _ = util_spec 'c', '1.0', 'b' => '= 2.0' + + e = assert_raises Gem::LoadError do + assert_activate nil, a, c, "b" + end + + assert_match(/Unable to activate c-1.0,/, e.message) + assert_match(/because b-1.0 conflicts with b .= 2.0/, e.message) + end + + ## + # DOC + + def test_self_activate_old_required + e1, = util_spec 'e', '1', 'd' => '= 1' + @d1 = util_spec 'd', '1' + @d2 = util_spec 'd', '2' + + assert_activate %w[d-1 e-1], e1, "d" + end + + ## + # DOC + + def test_self_activate_platform_alternate + @x1_m = util_spec 'x', '1' do |s| + s.platform = Gem::Platform.new %w[cpu my_platform 1] + end + + @x1_o = util_spec 'x', '1' do |s| + s.platform = Gem::Platform.new %w[cpu other_platform 1] + end + + @w1 = util_spec 'w', '1', 'x' => nil + + util_set_arch 'cpu-my_platform1' + + assert_activate %w[x-1-cpu-my_platform-1 w-1], @w1, @x1_m + end + + ## + # DOC + + def test_self_activate_platform_bump + @y1 = util_spec 'y', '1' + + @y1_1_p = util_spec 'y', '1.1' do |s| + s.platform = Gem::Platform.new %w[cpu my_platform 1] + end + + @z1 = util_spec 'z', '1', 'y' => nil + + assert_activate %w[y-1 z-1], @z1, @y1 + end + + ## + # [C] depends on + # [A] = 1.a + # [B] = 1.0 depends on + # [A] >= 0 (satisfied by 1.a) + + def test_self_activate_prerelease + @c1_pre = util_spec 'c', '1.a', "a" => "1.a", "b" => "1" + @a1_pre = util_spec 'a', '1.a' + @b1 = util_spec 'b', '1' do |s| + s.add_dependency 'a' + s.add_development_dependency 'aa' + end + + assert_activate %w[a-1.a b-1 c-1.a], @c1_pre, @a1_pre, @b1 + end + + def test_self_activate_via_require + a1 = new_spec "a", "1", "b" => "= 1" + b1 = new_spec "b", "1", nil, "lib/b/c.rb" + b2 = new_spec "b", "2", nil, "lib/b/c.rb" + + install_specs a1, b1, b2 + + a1.activate + save_loaded_features do + require "b/c" + end + + assert_equal %w(a-1 b-1), loaded_spec_names + end + + def test_self_activate_via_require_wtf + save_loaded_features do + a1 = new_spec "a", "1", "b" => "> 0", "d" => "> 0" # this + b1 = new_spec "b", "1", { "c" => ">= 1" }, "lib/b.rb" + b2 = new_spec "b", "2", { "c" => ">= 2" }, "lib/b.rb" # this + c1 = new_spec "c", "1" + c2 = new_spec "c", "2" # this + d1 = new_spec "d", "1", { "c" => "< 2" }, "lib/d.rb" + d2 = new_spec "d", "2", { "c" => "< 2" }, "lib/d.rb" # this + + install_specs a1, b1, b2, c1, c2, d1, d2 + + a1.activate + + assert_equal %w(a-1), loaded_spec_names + assert_equal ["b (> 0)", "d (> 0)"], unresolved_names + + require "b" + + e = assert_raises Gem::LoadError do + require "d" + end + + assert_equal "unable to find a version of 'd' to activate", e.message + + assert_equal %w(a-1 b-2 c-2), loaded_spec_names + assert_equal ["d (> 0)"], unresolved_names + end + end + + def test_self_activate_deep_unambiguous + a1 = new_spec "a", "1", "b" => "= 1" + b1 = new_spec "b", "1", "c" => "= 1" + b2 = new_spec "b", "2", "c" => "= 2" + c1 = new_spec "c", "1" + c2 = new_spec "c", "2" + + install_specs a1, b1, b2, c1, c2 + + a1.activate + assert_equal %w(a-1 b-1 c-1), loaded_spec_names + end + + def test_self_activate_loaded + foo = util_spec 'foo', '1' + + assert foo.activate + refute foo.activate + end + + ## + # [A] depends on + # [B] >= 1.0 (satisfied by 2.0) + # [C] depends on nothing + + def test_self_activate_unrelated + a = util_spec 'a', '1.0', 'b' => '>= 1.0' + util_spec 'b', '1.0' + c = util_spec 'c', '1.0' + + assert_activate %w[b-1.0 c-1.0 a-1.0], a, c, "b" + end + + ## + # [A] depends on + # [B] >= 1.0 (satisfied by 2.0) + # [C] = 1.0 depends on + # [B] ~> 1.0 + # + # and should resolve using b-1.0 + # TODO: move these to specification + + def test_self_activate_over + a = util_spec 'a', '1.0', 'b' => '>= 1.0', 'c' => '= 1.0' + util_spec 'b', '1.0' + util_spec 'b', '1.1' + util_spec 'b', '2.0' + util_spec 'c', '1.0', 'b' => '~> 1.0' + + a.activate + + assert_equal %w[a-1.0 c-1.0], loaded_spec_names + assert_equal ["b (>= 1.0, ~> 1.0)"], unresolved_names + end + + ## + # [A] depends on + # [B] ~> 1.0 (satisfied by 1.1) + # [C] = 1.0 depends on + # [B] = 1.0 + # + # and should resolve using b-1.0 + # + # TODO: this is not under, but over... under would require depth + # first resolve through a dependency that is later pruned. + + def test_self_activate_under + a, _ = util_spec 'a', '1.0', 'b' => '~> 1.0', 'c' => '= 1.0' + util_spec 'b', '1.0' + util_spec 'b', '1.1' + c, _ = util_spec 'c', '1.0', 'b' => '= 1.0' + + assert_activate %w[b-1.0 c-1.0 a-1.0], a, c, "b" + end + + ## + # [A1] depends on + # [B] > 0 (satisfied by 2.0) + # [B1] depends on + # [C] > 0 (satisfied by 1.0) + # [B2] depends on nothing! + # [C1] depends on nothing + + def test_self_activate_dropped + a1, = util_spec 'a', '1', 'b' => nil + util_spec 'b', '1', 'c' => nil + util_spec 'b', '2' + util_spec 'c', '1' + + assert_activate %w[b-2 a-1], a1, "b" + end + + ## + # [A] depends on + # [B] >= 1.0 (satisfied by 1.1) depends on + # [Z] + # [C] >= 1.0 depends on + # [B] = 1.0 + # + # and should backtrack to resolve using b-1.0, pruning Z from the + # resolve. + + def test_self_activate_raggi_the_edgecase_generator + a, _ = util_spec 'a', '1.0', 'b' => '>= 1.0', 'c' => '>= 1.0' + util_spec 'b', '1.0' + util_spec 'b', '1.1', 'z' => '>= 1.0' + c, _ = util_spec 'c', '1.0', 'b' => '= 1.0' + + assert_activate %w[b-1.0 c-1.0 a-1.0], a, c, "b" + end + + def test_self_activate_conflict + util_spec 'b', '1.0' + util_spec 'b', '2.0' + + gem "b", "= 1.0" + + assert_raises Gem::LoadError do + gem "b", "= 2.0" + end + end + def test_self_attribute_names expected_value = %w[ authors @@ -915,7 +1294,7 @@ dependencies: [] end def test_base_dir_not_loaded - @a1.instance_variable_set :@loaded_from, nil + @a1.instance_variable_set :@filename, nil assert_equal Gem.dir, @a1.base_dir end @@ -924,7 +1303,7 @@ dependencies: [] default_dir = File.join Gem::Specification.default_specifications_dir, @a1.spec_name - @a1.instance_variable_set :@loaded_from, default_dir + @a1.instance_variable_set :@filename, default_dir assert_equal Gem.default_dir, @a1.base_dir end @@ -1022,19 +1401,60 @@ dependencies: [] assert_equal %w[lib], @a1.require_paths end + def test_require_already_activated + save_loaded_features do + a1 = new_spec "a", "1", nil, "lib/d.rb" + + install_specs a1 # , a2, b1, b2, c1, c2 + + a1.activate + assert_equal %w(a-1), loaded_spec_names + assert_equal [], unresolved_names + + assert require "d" + + assert_equal %w(a-1), loaded_spec_names + assert_equal [], unresolved_names + end + end + + def test_require_already_activated_indirect_conflict + save_loaded_features do + a1 = new_spec "a", "1", "b" => "> 0" + a2 = new_spec "a", "2", "b" => "> 0" + b1 = new_spec "b", "1", "c" => ">= 1" + b2 = new_spec "b", "2", "c" => ">= 2" + c1 = new_spec "c", "1", nil, "lib/d.rb" + c2 = new_spec("c", "2", { "a" => "1" }, "lib/d.rb") # conflicts with a-2 + + install_specs a1, a2, b1, b2, c1, c2 + + a1.activate + c1.activate + assert_equal %w(a-1 c-1), loaded_spec_names + assert_equal ["b (> 0)"], unresolved_names + + assert require "d" + + assert_equal %w(a-1 c-1), loaded_spec_names + assert_equal ["b (> 0)"], unresolved_names + end + end + def test_requirements assert_equal ['A working computer'], @a1.requirements end def test_runtime_dependencies_legacy + make_spec_c1 # legacy gems don't have a type - @a1.runtime_dependencies.each do |dep| + @c1.runtime_dependencies.each do |dep| dep.instance_variable_set :@type, nil end expected = %w[rake jabber4r pqa] - assert_equal expected, @a1.runtime_dependencies.map { |d| d.name } + assert_equal expected, @c1.runtime_dependencies.map { |d| d.name } end def test_spaceship_name @@ -1088,11 +1508,13 @@ dependencies: [] @a2.add_runtime_dependency 'b', '1' @a2.dependencies.first.instance_variable_set :@type, nil @a2.required_rubygems_version = Gem::Requirement.new '> 0' + @a2.require_paths << "lib/a/ext" ruby_code = @a2.to_ruby expected = <<-SPEC # -*- encoding: utf-8 -*- +# stub: a 2 ruby lib\0lib/a/ext Gem::Specification.new do |s| s.name = "a" @@ -1105,7 +1527,7 @@ Gem::Specification.new do |s| s.email = "example@example.com" s.files = ["lib/code.rb"] s.homepage = "http://example.com" - s.require_paths = ["lib"] + s.require_paths = ["lib", "lib/a/ext"] s.rubygems_version = "#{Gem::VERSION}" s.summary = "this is a summary" @@ -1140,6 +1562,7 @@ end expected = <<-SPEC # -*- encoding: utf-8 -*- +# stub: a 2 ruby lib Gem::Specification.new do |s| s.name = "a" @@ -1179,14 +1602,17 @@ end end def test_to_ruby_fancy - @a1.platform = Gem::Platform.local - ruby_code = @a1.to_ruby + make_spec_c1 + + @c1.platform = Gem::Platform.local + ruby_code = @c1.to_ruby local = Gem::Platform.local expected_platform = "[#{local.cpu.inspect}, #{local.os.inspect}, #{local.version.inspect}]" expected = <<-SPEC # -*- encoding: utf-8 -*- +# stub: a 1 x86-darwin-8 lib Gem::Specification.new do |s| s.name = "a" @@ -1234,7 +1660,7 @@ end same_spec = eval ruby_code - assert_equal @a1, same_spec + assert_equal @c1, same_spec end def test_to_ruby_legacy @@ -1886,6 +2312,7 @@ end def test_metadata_specs valid_ruby_spec = <<-EOF # -*- encoding: utf-8 -*- +# stub: m 1 ruby lib Gem::Specification.new do |s| s.name = "m" diff --git a/test/rubygems/test_gem_stub_specification.rb b/test/rubygems/test_gem_stub_specification.rb new file mode 100644 index 0000000000..6feb96eb4f --- /dev/null +++ b/test/rubygems/test_gem_stub_specification.rb @@ -0,0 +1,30 @@ +require "rubygems/test_case" +require "rubygems/stub_specification" + +class TestStubSpecification < Gem::TestCase + SPECIFICATIONS = File.expand_path(File.join("..", "specifications"), __FILE__) + FOO = File.join SPECIFICATIONS, "foo-0.0.1.gemspec" + BAR = File.join SPECIFICATIONS, "bar-0.0.2.gemspec" + + def test_basic + stub = Gem::StubSpecification.new(FOO) + assert_equal "foo", stub.name + assert_equal Gem::Version.new("0.0.1"), stub.version + assert_equal Gem::Platform.new("mswin32"), stub.platform + assert_equal ["lib", "lib/f oo/ext"], stub.require_paths + end + + def test_missing_stubline + stub = Gem::StubSpecification.new(BAR) + assert_equal "bar", stub.name + assert_equal Gem::Version.new("0.0.2"), stub.version + assert_equal Gem::Platform.new("ruby"), stub.platform + assert_equal ["lib"], stub.require_paths + end + + def test_to_spec + stub = Gem::StubSpecification.new(FOO) + assert stub.to_spec.is_a?(Gem::Specification) + assert_equal "foo", stub.to_spec.name + end +end diff --git a/test/rubygems/test_gem_uninstaller.rb b/test/rubygems/test_gem_uninstaller.rb index 948318a5e3..1739614c67 100644 --- a/test/rubygems/test_gem_uninstaller.rb +++ b/test/rubygems/test_gem_uninstaller.rb @@ -5,6 +5,7 @@ class TestGemUninstaller < Gem::InstallerTestCase def setup super + common_installer_setup build_rake_in do use_ui ui do @@ -375,6 +376,19 @@ class TestGemUninstaller < Gem::InstallerTestCase assert_equal "Successfully uninstalled q-1.0", lines.shift end + def test_uninstall_doesnt_prompt_and_raises_when_abort_on_dependent_set + quick_gem 'r', '1' do |s| s.add_dependency 'q', '= 1' end + quick_gem 'q', '1' + + un = Gem::Uninstaller.new('q', :abort_on_dependent => true) + ui = Gem::MockGemUi.new("y\n") + + assert_raises Gem::DependencyRemovalException do + use_ui ui do + un.uninstall + end + end + end def test_uninstall_prompt_includes_dep_type quick_gem 'r', '1' do |s| diff --git a/test/rubygems/test_gem_uri_formatter.rb b/test/rubygems/test_gem_uri_formatter.rb new file mode 100644 index 0000000000..b185797a3a --- /dev/null +++ b/test/rubygems/test_gem_uri_formatter.rb @@ -0,0 +1,20 @@ +require 'rubygems/test_case' +require 'rubygems/uri_formatter' + +class TestGemUriFormatter < Gem::TestCase + + def test_normalize_uri + assert_equal 'FILE://example/', + Gem::UriFormatter.new('FILE://example/').normalize + assert_equal 'FTP://example/', + Gem::UriFormatter.new('FTP://example/').normalize + assert_equal 'HTTP://example/', + Gem::UriFormatter.new('HTTP://example/').normalize + assert_equal 'HTTPS://example/', + Gem::UriFormatter.new('HTTPS://example/').normalize + assert_equal 'http://example/', + Gem::UriFormatter.new('example/').normalize + end + +end + diff --git a/test/rubygems/test_gem_version.rb b/test/rubygems/test_gem_version.rb index da3b87dbca..2ba196e48d 100644 --- a/test/rubygems/test_gem_version.rb +++ b/test/rubygems/test_gem_version.rb @@ -122,6 +122,15 @@ class TestGemVersion < Gem::TestCase assert_equal "5.2.4", v("5.2.4").to_s end + def test_semver + assert_less_than "1.0.0-alpha", "1.0.0-alpha.1" + assert_less_than "1.0.0-alpha.1", "1.0.0-beta.2" + assert_less_than "1.0.0-beta.2", "1.0.0-beta.11" + assert_less_than "1.0.0-beta.11", "1.0.0-rc.1" + assert_less_than "1.0.0-rc1", "1.0.0" + assert_less_than "1.0.0-1", "1" + end + # Asserts that +version+ is a prerelease. def assert_prerelease version @@ -161,6 +170,12 @@ class TestGemVersion < Gem::TestCase assert second.eql?(first), "#{second} is eql? #{first}" end + def assert_less_than left, right + l = v(left) + r = v(right) + assert l < r, "#{left} not less than #{right}" + end + # Refute the assumption that +version+ is a prerelease. def refute_prerelease version diff --git a/test/rubygems/test_gem_version_option.rb b/test/rubygems/test_gem_version_option.rb index cbe819c22e..d6035ab800 100644 --- a/test/rubygems/test_gem_version_option.rb +++ b/test/rubygems/test_gem_version_option.rb @@ -80,7 +80,69 @@ class TestGemVersionOption < Gem::TestCase @cmd.handle_options %w[--version >1] - expected = { :version => Gem::Requirement.new('> 1'), :args => [] } + expected = { + :args => [], + :explicit_prerelease => false, + :prerelease => false, + :version => Gem::Requirement.new('> 1'), + } + + assert_equal expected, @cmd.options + end + + def test_version_option_compound + @cmd.add_version_option + + @cmd.handle_options ['--version', '< 1, > 0.9'] + + expected = { + :args => [], + :explicit_prerelease => false, + :prerelease => false, + :version => Gem::Requirement.new('< 1', '> 0.9'), + } + + assert_equal expected, @cmd.options + end + + def test_version_option_explicit_prerelease + @cmd.add_prerelease_option + @cmd.add_version_option + + @cmd.handle_options %w[--pre --version >1] + + expected = { + :args => [], + :explicit_prerelease => true, + :prerelease => true, + :version => Gem::Requirement.new('> 1'), + } + + assert_equal expected, @cmd.options + end + + def test_version_option_twice + @cmd.add_version_option + + @cmd.handle_options %w[--version >1.a] + + expected = { + :args => [], + :explicit_prerelease => false, + :prerelease => true, + :version => Gem::Requirement.new('> 1.a'), + } + + assert_equal expected, @cmd.options + + @cmd.handle_options %w[--version >1] + + expected = { + :args => [], + :explicit_prerelease => false, + :prerelease => false, + :version => Gem::Requirement.new('> 1'), + } assert_equal expected, @cmd.options end