From fbf59bdbea63efd34ccc144e648467d2f52e7345 Mon Sep 17 00:00:00 2001 From: drbrain Date: Sat, 10 Nov 2007 07:48:56 +0000 Subject: Import RubyGems trunk revision 1493. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@13862 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- ChangeLog | 12 + lib/rbconfig/datadir.rb | 24 + lib/rubygems.rb | 561 +++++++++++++ lib/rubygems/builder.rb | 81 ++ lib/rubygems/command.rb | 406 +++++++++ lib/rubygems/command_manager.rb | 144 ++++ lib/rubygems/commands/build_command.rb | 53 ++ lib/rubygems/commands/cert_command.rb | 86 ++ lib/rubygems/commands/check_command.rb | 74 ++ lib/rubygems/commands/cleanup_command.rb | 93 +++ lib/rubygems/commands/contents_command.rb | 74 ++ lib/rubygems/commands/dependency_command.rb | 150 ++++ lib/rubygems/commands/environment_command.rb | 80 ++ lib/rubygems/commands/fetch_command.rb | 62 ++ lib/rubygems/commands/generate_index_command.rb | 57 ++ lib/rubygems/commands/help_command.rb | 172 ++++ lib/rubygems/commands/install_command.rb | 125 +++ lib/rubygems/commands/list_command.rb | 35 + lib/rubygems/commands/lock_command.rb | 101 +++ lib/rubygems/commands/mirror_command.rb | 105 +++ lib/rubygems/commands/outdated_command.rb | 30 + lib/rubygems/commands/pristine_command.rb | 133 +++ lib/rubygems/commands/query_command.rb | 118 +++ lib/rubygems/commands/rdoc_command.rb | 78 ++ lib/rubygems/commands/search_command.rb | 37 + lib/rubygems/commands/server_command.rb | 48 ++ lib/rubygems/commands/sources_command.rb | 115 +++ lib/rubygems/commands/specification_command.rb | 72 ++ lib/rubygems/commands/uninstall_command.rb | 56 ++ lib/rubygems/commands/unpack_command.rb | 76 ++ lib/rubygems/commands/update_command.rb | 149 ++++ lib/rubygems/commands/which_command.rb | 86 ++ lib/rubygems/config_file.rb | 224 +++++ lib/rubygems/custom_require.rb | 38 + lib/rubygems/dependency.rb | 65 ++ lib/rubygems/dependency_installer.rb | 219 +++++ lib/rubygems/dependency_list.rb | 165 ++++ lib/rubygems/digest/digest_adapter.rb | 40 + lib/rubygems/digest/md5.rb | 23 + lib/rubygems/digest/sha1.rb | 17 + lib/rubygems/digest/sha2.rb | 17 + lib/rubygems/doc_manager.rb | 161 ++++ lib/rubygems/exceptions.rb | 63 ++ lib/rubygems/ext.rb | 18 + lib/rubygems/ext/builder.rb | 56 ++ lib/rubygems/ext/configure_builder.rb | 24 + lib/rubygems/ext/ext_conf_builder.rb | 23 + lib/rubygems/ext/rake_builder.rb | 27 + lib/rubygems/format.rb | 81 ++ lib/rubygems/gem_open_uri.rb | 7 + lib/rubygems/gem_openssl.rb | 83 ++ lib/rubygems/gem_path_searcher.rb | 84 ++ lib/rubygems/gem_runner.rb | 58 ++ lib/rubygems/indexer.rb | 171 ++++ lib/rubygems/indexer/abstract_index_builder.rb | 80 ++ lib/rubygems/indexer/marshal_index_builder.rb | 8 + lib/rubygems/indexer/master_index_builder.rb | 44 + lib/rubygems/indexer/quick_index_builder.rb | 48 ++ lib/rubygems/install_update_options.rb | 87 ++ lib/rubygems/installer.rb | 421 ++++++++++ lib/rubygems/local_remote_options.rb | 106 +++ lib/rubygems/old_format.rb | 148 ++++ lib/rubygems/open-uri.rb | 773 ++++++++++++++++++ lib/rubygems/package.rb | 851 +++++++++++++++++++ lib/rubygems/platform.rb | 187 +++++ lib/rubygems/remote_fetcher.rb | 164 ++++ lib/rubygems/remote_installer.rb | 195 +++++ lib/rubygems/requirement.rb | 157 ++++ lib/rubygems/rubygems_version.rb | 6 + lib/rubygems/security.rb | 785 ++++++++++++++++++ lib/rubygems/server.rb | 504 ++++++++++++ lib/rubygems/source_index.rb | 446 ++++++++++ lib/rubygems/source_info_cache.rb | 232 ++++++ lib/rubygems/source_info_cache_entry.rb | 46 ++ lib/rubygems/specification.rb | 905 +++++++++++++++++++++ lib/rubygems/timer.rb | 25 + lib/rubygems/uninstaller.rb | 183 +++++ lib/rubygems/user_interaction.rb | 291 +++++++ lib/rubygems/validator.rb | 185 +++++ lib/rubygems/version.rb | 158 ++++ lib/rubygems/version_option.rb | 49 ++ lib/ubygems.rb | 10 + test/rubygems/bogussources.rb | 8 + test/rubygems/data/gem-private_key.pem | 27 + test/rubygems/data/gem-public_cert.pem | 20 + test/rubygems/fake_certlib/openssl.rb | 7 + test/rubygems/functional.rb | 95 +++ test/rubygems/gemutilities.rb | 295 +++++++ test/rubygems/insure_session.rb | 51 ++ test/rubygems/mockgemui.rb | 51 ++ test/rubygems/simple_gem.rb | 72 ++ test/rubygems/test_config.rb | 26 + test/rubygems/test_gem.rb | 367 +++++++++ test/rubygems/test_gem_builder.rb | 34 + test/rubygems/test_gem_command.rb | 196 +++++ test/rubygems/test_gem_command_manager.rb | 211 +++++ test/rubygems/test_gem_commands_build_command.rb | 75 ++ test/rubygems/test_gem_commands_cert_command.rb | 122 +++ test/rubygems/test_gem_commands_check_command.rb | 25 + .../rubygems/test_gem_commands_contents_command.rb | 92 +++ .../test_gem_commands_dependency_command.rb | 108 +++ .../test_gem_commands_environment_command.rb | 116 +++ test/rubygems/test_gem_commands_fetch_command.rb | 34 + .../test_gem_commands_generate_index_command.rb | 32 + test/rubygems/test_gem_commands_install_command.rb | 160 ++++ test/rubygems/test_gem_commands_mirror_command.rb | 56 ++ .../rubygems/test_gem_commands_pristine_command.rb | 100 +++ test/rubygems/test_gem_commands_query_command.rb | 82 ++ test/rubygems/test_gem_commands_sources_command.rb | 147 ++++ .../test_gem_commands_specification_command.rb | 93 +++ test/rubygems/test_gem_commands_unpack_command.rb | 55 ++ test/rubygems/test_gem_config_file.rb | 210 +++++ test/rubygems/test_gem_dependency.rb | 89 ++ test/rubygems/test_gem_dependency_installer.rb | 519 ++++++++++++ test/rubygems/test_gem_dependency_list.rb | 212 +++++ test/rubygems/test_gem_digest.rb | 44 + test/rubygems/test_gem_doc_manager.rb | 32 + test/rubygems/test_gem_ext_configure_builder.rb | 84 ++ test/rubygems/test_gem_ext_ext_conf_builder.rb | 122 +++ test/rubygems/test_gem_ext_rake_builder.rb | 73 ++ test/rubygems/test_gem_format.rb | 51 ++ test/rubygems/test_gem_gem_path_searcher.rb | 57 ++ test/rubygems/test_gem_gem_runner.rb | 35 + test/rubygems/test_gem_indexer.rb | 103 +++ test/rubygems/test_gem_install_update_options.rb | 40 + test/rubygems/test_gem_installer.rb | 796 ++++++++++++++++++ test/rubygems/test_gem_local_remote_options.rb | 84 ++ test/rubygems/test_gem_outdated_command.rb | 40 + test/rubygems/test_gem_platform.rb | 239 ++++++ test/rubygems/test_gem_remote_fetcher.rb | 417 ++++++++++ test/rubygems/test_gem_remote_installer.rb | 161 ++++ test/rubygems/test_gem_requirement.rb | 223 +++++ test/rubygems/test_gem_server.rb | 71 ++ test/rubygems/test_gem_source_index.rb | 429 ++++++++++ test/rubygems/test_gem_source_info_cache.rb | 299 +++++++ test/rubygems/test_gem_source_info_cache_entry.rb | 46 ++ test/rubygems/test_gem_specification.rb | 707 ++++++++++++++++ test/rubygems/test_gem_stream_ui.rb | 117 +++ test/rubygems/test_gem_validator.rb | 70 ++ test/rubygems/test_gem_version.rb | 191 +++++ test/rubygems/test_gem_version_option.rb | 77 ++ test/rubygems/test_kernel.rb | 64 ++ test/rubygems/test_open_uri.rb | 13 + test/rubygems/test_package.rb | 607 ++++++++++++++ 144 files changed, 21330 insertions(+) create mode 100644 lib/rbconfig/datadir.rb create mode 100644 lib/rubygems.rb create mode 100644 lib/rubygems/builder.rb create mode 100644 lib/rubygems/command.rb create mode 100644 lib/rubygems/command_manager.rb create mode 100644 lib/rubygems/commands/build_command.rb create mode 100644 lib/rubygems/commands/cert_command.rb create mode 100644 lib/rubygems/commands/check_command.rb create mode 100644 lib/rubygems/commands/cleanup_command.rb create mode 100644 lib/rubygems/commands/contents_command.rb create mode 100644 lib/rubygems/commands/dependency_command.rb create mode 100644 lib/rubygems/commands/environment_command.rb create mode 100644 lib/rubygems/commands/fetch_command.rb create mode 100644 lib/rubygems/commands/generate_index_command.rb create mode 100644 lib/rubygems/commands/help_command.rb create mode 100644 lib/rubygems/commands/install_command.rb create mode 100644 lib/rubygems/commands/list_command.rb create mode 100644 lib/rubygems/commands/lock_command.rb create mode 100644 lib/rubygems/commands/mirror_command.rb create mode 100644 lib/rubygems/commands/outdated_command.rb create mode 100644 lib/rubygems/commands/pristine_command.rb create mode 100644 lib/rubygems/commands/query_command.rb create mode 100644 lib/rubygems/commands/rdoc_command.rb create mode 100644 lib/rubygems/commands/search_command.rb create mode 100644 lib/rubygems/commands/server_command.rb create mode 100644 lib/rubygems/commands/sources_command.rb create mode 100644 lib/rubygems/commands/specification_command.rb create mode 100644 lib/rubygems/commands/uninstall_command.rb create mode 100644 lib/rubygems/commands/unpack_command.rb create mode 100644 lib/rubygems/commands/update_command.rb create mode 100644 lib/rubygems/commands/which_command.rb create mode 100644 lib/rubygems/config_file.rb create mode 100755 lib/rubygems/custom_require.rb create mode 100644 lib/rubygems/dependency.rb create mode 100644 lib/rubygems/dependency_installer.rb create mode 100644 lib/rubygems/dependency_list.rb create mode 100755 lib/rubygems/digest/digest_adapter.rb create mode 100755 lib/rubygems/digest/md5.rb create mode 100755 lib/rubygems/digest/sha1.rb create mode 100755 lib/rubygems/digest/sha2.rb create mode 100644 lib/rubygems/doc_manager.rb create mode 100644 lib/rubygems/exceptions.rb create mode 100644 lib/rubygems/ext.rb create mode 100644 lib/rubygems/ext/builder.rb create mode 100644 lib/rubygems/ext/configure_builder.rb create mode 100644 lib/rubygems/ext/ext_conf_builder.rb create mode 100644 lib/rubygems/ext/rake_builder.rb create mode 100644 lib/rubygems/format.rb create mode 100644 lib/rubygems/gem_open_uri.rb create mode 100644 lib/rubygems/gem_openssl.rb create mode 100644 lib/rubygems/gem_path_searcher.rb create mode 100644 lib/rubygems/gem_runner.rb create mode 100644 lib/rubygems/indexer.rb create mode 100644 lib/rubygems/indexer/abstract_index_builder.rb create mode 100644 lib/rubygems/indexer/marshal_index_builder.rb create mode 100644 lib/rubygems/indexer/master_index_builder.rb create mode 100644 lib/rubygems/indexer/quick_index_builder.rb create mode 100644 lib/rubygems/install_update_options.rb create mode 100644 lib/rubygems/installer.rb create mode 100644 lib/rubygems/local_remote_options.rb create mode 100644 lib/rubygems/old_format.rb create mode 100644 lib/rubygems/open-uri.rb create mode 100644 lib/rubygems/package.rb create mode 100644 lib/rubygems/platform.rb create mode 100644 lib/rubygems/remote_fetcher.rb create mode 100644 lib/rubygems/remote_installer.rb create mode 100644 lib/rubygems/requirement.rb create mode 100644 lib/rubygems/rubygems_version.rb create mode 100644 lib/rubygems/security.rb create mode 100644 lib/rubygems/server.rb create mode 100644 lib/rubygems/source_index.rb create mode 100644 lib/rubygems/source_info_cache.rb create mode 100644 lib/rubygems/source_info_cache_entry.rb create mode 100644 lib/rubygems/specification.rb create mode 100755 lib/rubygems/timer.rb create mode 100644 lib/rubygems/uninstaller.rb create mode 100644 lib/rubygems/user_interaction.rb create mode 100755 lib/rubygems/validator.rb create mode 100644 lib/rubygems/version.rb create mode 100644 lib/rubygems/version_option.rb create mode 100644 lib/ubygems.rb create mode 100644 test/rubygems/bogussources.rb create mode 100644 test/rubygems/data/gem-private_key.pem create mode 100644 test/rubygems/data/gem-public_cert.pem create mode 100644 test/rubygems/fake_certlib/openssl.rb create mode 100644 test/rubygems/functional.rb create mode 100644 test/rubygems/gemutilities.rb create mode 100644 test/rubygems/insure_session.rb create mode 100644 test/rubygems/mockgemui.rb create mode 100644 test/rubygems/simple_gem.rb create mode 100644 test/rubygems/test_config.rb create mode 100644 test/rubygems/test_gem.rb create mode 100644 test/rubygems/test_gem_builder.rb create mode 100644 test/rubygems/test_gem_command.rb create mode 100644 test/rubygems/test_gem_command_manager.rb create mode 100644 test/rubygems/test_gem_commands_build_command.rb create mode 100644 test/rubygems/test_gem_commands_cert_command.rb create mode 100644 test/rubygems/test_gem_commands_check_command.rb create mode 100644 test/rubygems/test_gem_commands_contents_command.rb create mode 100644 test/rubygems/test_gem_commands_dependency_command.rb create mode 100644 test/rubygems/test_gem_commands_environment_command.rb create mode 100644 test/rubygems/test_gem_commands_fetch_command.rb create mode 100644 test/rubygems/test_gem_commands_generate_index_command.rb create mode 100644 test/rubygems/test_gem_commands_install_command.rb create mode 100644 test/rubygems/test_gem_commands_mirror_command.rb create mode 100644 test/rubygems/test_gem_commands_pristine_command.rb create mode 100644 test/rubygems/test_gem_commands_query_command.rb create mode 100644 test/rubygems/test_gem_commands_sources_command.rb create mode 100644 test/rubygems/test_gem_commands_specification_command.rb create mode 100644 test/rubygems/test_gem_commands_unpack_command.rb create mode 100644 test/rubygems/test_gem_config_file.rb create mode 100644 test/rubygems/test_gem_dependency.rb create mode 100644 test/rubygems/test_gem_dependency_installer.rb create mode 100644 test/rubygems/test_gem_dependency_list.rb create mode 100755 test/rubygems/test_gem_digest.rb create mode 100644 test/rubygems/test_gem_doc_manager.rb create mode 100644 test/rubygems/test_gem_ext_configure_builder.rb create mode 100644 test/rubygems/test_gem_ext_ext_conf_builder.rb create mode 100644 test/rubygems/test_gem_ext_rake_builder.rb create mode 100644 test/rubygems/test_gem_format.rb create mode 100644 test/rubygems/test_gem_gem_path_searcher.rb create mode 100644 test/rubygems/test_gem_gem_runner.rb create mode 100644 test/rubygems/test_gem_indexer.rb create mode 100644 test/rubygems/test_gem_install_update_options.rb create mode 100644 test/rubygems/test_gem_installer.rb create mode 100644 test/rubygems/test_gem_local_remote_options.rb create mode 100644 test/rubygems/test_gem_outdated_command.rb create mode 100644 test/rubygems/test_gem_platform.rb create mode 100644 test/rubygems/test_gem_remote_fetcher.rb create mode 100644 test/rubygems/test_gem_remote_installer.rb create mode 100644 test/rubygems/test_gem_requirement.rb create mode 100644 test/rubygems/test_gem_server.rb create mode 100644 test/rubygems/test_gem_source_index.rb create mode 100644 test/rubygems/test_gem_source_info_cache.rb create mode 100644 test/rubygems/test_gem_source_info_cache_entry.rb create mode 100644 test/rubygems/test_gem_specification.rb create mode 100644 test/rubygems/test_gem_stream_ui.rb create mode 100644 test/rubygems/test_gem_validator.rb create mode 100644 test/rubygems/test_gem_version.rb create mode 100644 test/rubygems/test_gem_version_option.rb create mode 100644 test/rubygems/test_kernel.rb create mode 100644 test/rubygems/test_open_uri.rb create mode 100644 test/rubygems/test_package.rb diff --git a/ChangeLog b/ChangeLog index cf526b64d3..c1944b076a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,15 @@ +Sat Nov 10 16:37:07 2007 Eric Hodel + + * lib/rubygems: Import RubyGems revision 1493. + + * lib/rubygems.rb: ditto. + + * lib/ubygems.rb: ditto. + + * lib/rbconfig/datadir.rb: ditto. + + * test/rubygems: ditto. + Sat Nov 10 16:34:21 2007 Eric Hodel * lib/soap/property.rb: Don't override Enumerable#inject for 1.9. diff --git a/lib/rbconfig/datadir.rb b/lib/rbconfig/datadir.rb new file mode 100644 index 0000000000..5b8f07754a --- /dev/null +++ b/lib/rbconfig/datadir.rb @@ -0,0 +1,24 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + + +module Config + + # Only define datadir if it doesn't already exist. + unless Config.respond_to?(:datadir) + + # Return the path to the data directory associated with the given + # package name. Normally this is just + # "#{Config::CONFIG['datadir']}/#{package_name}", but may be + # modified by packages like RubyGems to handle versioned data + # directories. + def Config.datadir(package_name) + File.join(CONFIG['datadir'], package_name) + end + + end +end diff --git a/lib/rubygems.rb b/lib/rubygems.rb new file mode 100644 index 0000000000..3ee9593ce3 --- /dev/null +++ b/lib/rubygems.rb @@ -0,0 +1,561 @@ +# -*- ruby -*- +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rbconfig' +require 'rubygems/rubygems_version' +require 'thread' + +module Gem + class LoadError < ::LoadError + attr_accessor :name, :version_requirement + end +end + +module Kernel + + # Adds a Ruby Gem to the $LOAD_PATH. Before a Gem is loaded, its + # required Gems are loaded. If the version information is omitted, + # the highest version Gem of the supplied name is loaded. If a Gem + # is not found that meets the version requirement and/or a required + # Gem is not found, a Gem::LoadError is raised. More information on + # version requirements can be found in the Gem::Version + # documentation. + # + # The +gem+ directive should be executed *before* any require + # statements (otherwise rubygems might select a conflicting library + # version). + # + # You can define the environment variable GEM_SKIP as a way to not + # load specified gems. you might do this to test out changes that + # haven't been intsalled yet. Example: + # + # GEM_SKIP=libA:libB ruby-I../libA -I../libB ./mycode.rb + # + # gem:: [String or Gem::Dependency] The gem name or dependency + # instance. + # + # version_requirement:: [default=">= 0"] The version + # requirement. + # + # return:: [Boolean] true if the Gem is loaded, otherwise false. + # + # raises:: [Gem::LoadError] if Gem cannot be found, is listed in + # GEM_SKIP, or version requirement not met. + # + def gem(gem_name, *version_requirements) + active_gem_with_options(gem_name, version_requirements) + end + + # Same as the +gem+ command, but will also require a file if the gem + # provides an auto-required file name. + # + # DEPRECATED! Use +gem+ instead. + # + def require_gem(gem_name, *version_requirements) + file, lineno = location_of_caller + warn "#{file}:#{lineno}:Warning: require_gem is obsolete. Use gem instead." + active_gem_with_options(gem_name, version_requirements, :auto_require=>true) + end + + # Return the file name (string) and line number (integer) of the caller of + # the caller of this method. + def location_of_caller + file, lineno = caller[1].split(':') + lineno = lineno.to_i + [file, lineno] + end + private :location_of_caller + + def active_gem_with_options(gem_name, version_requirements, options={}) + skip_list = (ENV['GEM_SKIP'] || "").split(/:/) + raise Gem::LoadError, "skipping #{gem_name}" if skip_list.include? gem_name + Gem.activate(gem_name, options[:auto_require], *version_requirements) + end + private :active_gem_with_options +end + +# Main module to hold all RubyGem classes/modules. +# +module Gem + + MUTEX = Mutex.new + + RubyGemsPackageVersion = RubyGemsVersion + + DIRECTORIES = %w[cache doc gems specifications] + + @@source_index = nil + @@win_platform = nil + + @configuration = nil + @loaded_specs = {} + @platforms = nil + @ruby = nil + @sources = [] + + # Reset the +dir+ and +path+ values. The next time +dir+ or +path+ + # is requested, the values will be calculated from scratch. This is + # mainly used by the unit tests to provide test isolation. + # + def self.clear_paths + @gem_home = nil + @gem_path = nil + @@source_index = nil + MUTEX.synchronize do + @searcher = nil + end + end + + # The version of the Marshal format for your Ruby. + def self.marshal_version + "#{Marshal::MAJOR_VERSION}.#{Marshal::MINOR_VERSION}" + end + + ## + # The directory prefix this RubyGems was installed at. + + def self.prefix + prefix = File.dirname File.expand_path(__FILE__) + if prefix == Config::CONFIG['sitelibdir'] then + nil + else + File.dirname prefix + end + end + + # Returns an Cache of specifications that are in the Gem.path + # + # return:: [Gem::SourceIndex] Index of installed Gem::Specifications + # + def self.source_index + @@source_index ||= SourceIndex.from_installed_gems + end + + ## + # An Array of Regexps that match windows ruby platforms. + + WIN_PATTERNS = [/mswin/i, /mingw/i, /bccwin/i, /wince/i] + + ## + # Is this a windows platform? + + def self.win_platform? + if @@win_platform.nil? then + @@win_platform = !!WIN_PATTERNS.find { |r| RUBY_PLATFORM =~ r } + end + + @@win_platform + end + + class << self + + attr_reader :loaded_specs + + # Quietly ensure the named Gem directory contains all the proper + # subdirectories. If we can't create a directory due to a permission + # problem, then we will silently continue. + def ensure_gem_subdirectories(gemdir) + require 'fileutils' + + Gem::DIRECTORIES.each do |filename| + fn = File.join gemdir, filename + FileUtils.mkdir_p fn rescue nil unless File.exist? fn + end + end + + def platforms + @platforms ||= [Gem::Platform::RUBY, Gem::Platform.local] + end + + # Returns an Array of sources to fetch remote gems from. If the sources + # list is empty, attempts to load the "sources" gem, then uses + # default_sources if it is not installed. + def sources + if @sources.empty? then + begin + gem 'sources', '> 0.0.1' + require 'sources' + rescue LoadError + @sources = default_sources + end + end + + @sources + end + + # An Array of the default sources that come with RubyGems. + def default_sources + %w[http://gems.rubyforge.org] + end + + # Provide an alias for the old name. + alias cache source_index + + # The directory path where Gems are to be installed. + # + # return:: [String] The directory path + # + def dir + @gem_home ||= nil + set_home(ENV['GEM_HOME'] || default_dir) unless @gem_home + @gem_home + end + + # The directory path where executables are to be installed. + # + def bindir(install_dir=Gem.dir) + return File.join(install_dir, 'bin') unless + install_dir.to_s == Gem.default_dir + + if defined? RUBY_FRAMEWORK_VERSION then # mac framework support + File.join(File.dirname(Config::CONFIG["sitedir"]), + File.basename(Config::CONFIG["bindir"])) + else # generic install + Config::CONFIG['bindir'] + end + end + + # List of directory paths to search for Gems. + # + # return:: [List] List of directory paths. + # + def path + @gem_path ||= nil + set_paths(ENV['GEM_PATH']) unless @gem_path + @gem_path + end + + # The home directory for the user. + def user_home + @user_home ||= find_home + end + + # Return the path to standard location of the users .gemrc file. + def config_file + File.join(Gem.user_home, '.gemrc') + end + + # The standard configuration object for gems. + def configuration + return @configuration if @configuration + require 'rubygems/config_file' + @configuration = Gem::ConfigFile.new [] + end + + # Use the given configuration object (which implements the + # ConfigFile protocol) as the standard configuration object. + def configuration=(config) + @configuration = config + end + + # Return the path the the data directory specified by the gem + # name. If the package is not available as a gem, return nil. + def datadir(gem_name) + spec = @loaded_specs[gem_name] + return nil if spec.nil? + File.join(spec.full_gem_path, 'data', gem_name) + end + + # Return the searcher object to search for matching gems. + def searcher + MUTEX.synchronize do + @searcher ||= Gem::GemPathSearcher.new + end + end + + # Return the Ruby command to use to execute the Ruby interpreter. + def ruby + if @ruby.nil? then + @ruby = File.join(Config::CONFIG['bindir'], + Config::CONFIG['ruby_install_name']) + @ruby << Config::CONFIG['EXEEXT'] + end + + @ruby + end + + # Activate a gem (i.e. add it to the Ruby load path). The gem + # must satisfy all the specified version constraints. If + # +autorequire+ is true, then automatically require the specified + # autorequire file in the gem spec. + # + # Returns true if the gem is loaded by this call, false if it is + # already loaded, or an exception otherwise. + # + def activate(gem, autorequire, *version_requirements) + if version_requirements.empty? then + version_requirements = Gem::Requirement.default + end + + unless gem.respond_to?(:name) && gem.respond_to?(:version_requirements) + gem = Gem::Dependency.new(gem, version_requirements) + end + + matches = Gem.source_index.find_name(gem.name, gem.version_requirements) + report_activate_error(gem) if matches.empty? + + if @loaded_specs[gem.name] + # This gem is already loaded. If the currently loaded gem is + # not in the list of candidate gems, then we have a version + # conflict. + existing_spec = @loaded_specs[gem.name] + if ! matches.any? { |spec| spec.version == existing_spec.version } + fail Gem::Exception, "can't activate #{gem}, already activated #{existing_spec.full_name}]" + end + return false + end + + # new load + spec = matches.last + if spec.loaded? + return false unless autorequire + result = spec.autorequire ? require(spec.autorequire) : false + return result || false + end + + spec.loaded = true + @loaded_specs[spec.name] = spec + + # Load dependent gems first + spec.dependencies.each do |dep_gem| + activate(dep_gem, autorequire) + end + + # bin directory must come before library directories + spec.require_paths.unshift spec.bindir if spec.bindir + + require_paths = spec.require_paths.map do |path| + File.join spec.full_gem_path, path + end + + sitelibdir = Config::CONFIG['sitelibdir'] + + # gem directories must come after -I and ENV['RUBYLIB'] + $:.insert($:.index(sitelibdir), *require_paths) + + # Now autorequire + if autorequire && spec.autorequire then # DEPRECATED + Array(spec.autorequire).each do |a_lib| + require a_lib + end + end + + return true + end + + # Report a load error during activation. The message of load + # error depends on whether it was a version mismatch or if there + # are not gems of any version by the requested name. + def report_activate_error(gem) + matches = Gem.source_index.find_name(gem.name) + + if matches.empty? then + error = Gem::LoadError.new( + "Could not find RubyGem #{gem.name} (#{gem.version_requirements})\n") + else + error = Gem::LoadError.new( + "RubyGem version error: " + + "#{gem.name}(#{matches.first.version} not #{gem.version_requirements})\n") + end + + error.name = gem.name + error.version_requirement = gem.version_requirements + raise error + end + private :report_activate_error + + # Use the +home+ and (optional) +paths+ values for +dir+ and +path+. + # Used mainly by the unit tests to provide environment isolation. + # + def use_paths(home, paths=[]) + clear_paths + set_home(home) if home + set_paths(paths.join(File::PATH_SEPARATOR)) if paths + end + + # Return a list of all possible load paths for all versions for + # all gems in the Gem installation. + # + def all_load_paths + result = [] + Gem.path.each do |gemdir| + each_load_path(all_partials(gemdir)) do |load_path| + result << load_path + end + end + result + end + + # Return a list of all possible load paths for the latest version + # for all gems in the Gem installation. + def latest_load_paths + result = [] + Gem.path.each do |gemdir| + each_load_path(latest_partials(gemdir)) do |load_path| + result << load_path + end + end + result + end + + def required_location(gemname, libfile, *version_constraints) + version_constraints = Gem::Requirement.default if version_constraints.empty? + matches = Gem.source_index.find_name(gemname, version_constraints) + return nil if matches.empty? + spec = matches.last + spec.require_paths.each do |path| + result = File.join(spec.full_gem_path, path, libfile) + return result if File.exist?(result) + end + nil + end + + def suffixes + ['', '.rb', '.rbw', '.so', '.bundle', '.dll', '.sl', '.jar'] + end + + def suffix_pattern + @suffix_pattern ||= "{#{suffixes.join(',')}}" + end + + # manage_gems is useless and deprecated. Don't call it anymore. This + # will warn in two releases. + def manage_gems + # do nothing + end + + private + + # Return all the partial paths in the given +gemdir+. + def all_partials(gemdir) + Dir[File.join(gemdir, 'gems/*')] + end + + # Return only the latest partial paths in the given +gemdir+. + def latest_partials(gemdir) + latest = {} + all_partials(gemdir).each do |gp| + base = File.basename(gp) + if base =~ /(.*)-((\d+\.)*\d+)/ then + name, version = $1, $2 + ver = Gem::Version.new(version) + if latest[name].nil? || ver > latest[name][0] + latest[name] = [ver, gp] + end + end + end + latest.collect { |k,v| v[1] } + end + + # Expand each partial gem path with each of the required paths + # specified in the Gem spec. Each expanded path is yielded. + def each_load_path(partials) + partials.each do |gp| + base = File.basename(gp) + specfn = File.join(dir, "specifications", base + ".gemspec") + if File.exist?(specfn) + spec = eval(File.read(specfn)) + spec.require_paths.each do |rp| + yield(File.join(gp, rp)) + end + else + filename = File.join(gp, 'lib') + yield(filename) if File.exist?(filename) + end + end + end + + # Set the Gem home directory (as reported by +dir+). + def set_home(home) + @gem_home = home + ensure_gem_subdirectories(@gem_home) + end + + # Set the Gem search path (as reported by +path+). + def set_paths(gpaths) + if gpaths + @gem_path = gpaths.split(File::PATH_SEPARATOR) + @gem_path << Gem.dir + else + @gem_path = [Gem.dir] + end + @gem_path.uniq! + @gem_path.each do |gp| ensure_gem_subdirectories(gp) end + end + + # Some comments from the ruby-talk list regarding finding the home + # directory: + # + # I have HOME, USERPROFILE and HOMEDRIVE + HOMEPATH. Ruby seems + # to be depending on HOME in those code samples. I propose that + # it should fallback to USERPROFILE and HOMEDRIVE + HOMEPATH (at + # least on Win32). + # + def find_home + ['HOME', 'USERPROFILE'].each do |homekey| + return ENV[homekey] if ENV[homekey] + end + if ENV['HOMEDRIVE'] && ENV['HOMEPATH'] + return "#{ENV['HOMEDRIVE']}:#{ENV['HOMEPATH']}" + end + begin + File.expand_path("~") + rescue StandardError => ex + if File::ALT_SEPARATOR + "C:/" + else + "/" + end + end + end + + public + + # Default home directory path to be used if an alternate value is + # not specified in the environment. + def default_dir + if defined? RUBY_FRAMEWORK_VERSION + return File.join(File.dirname(Config::CONFIG["sitedir"]), "Gems") + else + File.join(Config::CONFIG['libdir'], 'ruby', 'gems', Config::CONFIG['ruby_version']) + end + end + + end + +end + +# Modify the non-gem version of datadir to handle gem package names. + +require 'rbconfig/datadir' +module Config # :nodoc: + class << self + alias gem_original_datadir datadir + + # Return the path to the data directory associated with the named + # package. If the package is loaded as a gem, return the gem + # specific data directory. Otherwise return a path to the share + # area as define by "#{Config::CONFIG['datadir']}/#{package_name}". + def datadir(package_name) + Gem.datadir(package_name) || Config.gem_original_datadir(package_name) + end + end +end + +require 'rubygems/exceptions' +require 'rubygems/version' +require 'rubygems/requirement' +require 'rubygems/dependency' +require 'rubygems/gem_path_searcher' # Needed for Kernel#gem +require 'rubygems/source_index' # Needed for Kernel#gem +require 'rubygems/platform' +require 'rubygems/builder' # HACK: Needed for rake's package task. + +if RUBY_VERSION < '1.9' then + require 'rubygems/custom_require' +end + diff --git a/lib/rubygems/builder.rb b/lib/rubygems/builder.rb new file mode 100644 index 0000000000..f7f07e86bf --- /dev/null +++ b/lib/rubygems/builder.rb @@ -0,0 +1,81 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +module Gem + + ## + # The Builder class processes RubyGem specification files + # to produce a .gem file. + # + class Builder + + include UserInteraction + ## + # Constructs a builder instance for the provided specification + # + # spec:: [Gem::Specification] The specification instance + # + def initialize(spec) + require "yaml" + require "rubygems/package" + require "rubygems/security" + + @spec = spec + end + + ## + # Builds the gem from the specification. Returns the name of the file + # written. + # + def build + @spec.mark_version + @spec.validate + @signer = sign + write_package + say success + @spec.file_name + end + + def success + <<-EOM + Successfully built RubyGem + Name: #{@spec.name} + Version: #{@spec.version} + File: #{@spec.full_name+'.gem'} +EOM + end + + private + + def sign + # if the signing key was specified, then load the file, and swap + # to the public key (TODO: we should probably just omit the + # signing key in favor of the signing certificate, but that's for + # the future, also the signature algorithm should be configurable) + signer = nil + if @spec.respond_to?(:signing_key) && @spec.signing_key + signer = Gem::Security::Signer.new(@spec.signing_key, @spec.cert_chain) + @spec.signing_key = nil + @spec.cert_chain = signer.cert_chain.map { |cert| cert.to_s } + end + signer + end + + def write_package + Package.open(@spec.file_name, "w", @signer) do |pkg| + pkg.metadata = @spec.to_yaml + @spec.files.each do |file| + next if File.directory? file + pkg.add_file_simple(file, File.stat(@spec.file_name).mode & 0777, + File.size(file)) do |os| + os.write File.open(file, "rb"){|f|f.read} + end + end + end + end + end +end + diff --git a/lib/rubygems/command.rb b/lib/rubygems/command.rb new file mode 100644 index 0000000000..66855c7c6a --- /dev/null +++ b/lib/rubygems/command.rb @@ -0,0 +1,406 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'optparse' + +require 'rubygems/user_interaction' + +module Gem + + # Base class for all Gem commands. When creating a new gem command, define + # #arguments, #defaults_str, #description and #usage (as appropriate). + class Command + + include UserInteraction + + # The name of the command. + attr_reader :command + + # The options for the command. + attr_reader :options + + # The default options for the command. + attr_accessor :defaults + + # The name of the command for command-line invocation. + attr_accessor :program_name + + # A short description of the command. + attr_accessor :summary + + # Initializes a generic gem command named +command+. +summary+ is a short + # description displayed in `gem help commands`. +defaults+ are the + # default options. Defaults should be mirrored in #defaults_str, unless + # there are none. + # + # Use add_option to add command-line switches. + def initialize(command, summary=nil, defaults={}) + @command = command + @summary = summary + @program_name = "gem #{command}" + @defaults = defaults + @options = defaults.dup + @option_groups = Hash.new { |h,k| h[k] = [] } + @parser = nil + @when_invoked = nil + end + + # True if +long+ begins with the characters from +short+. + def begins?(long, short) + return false if short.nil? + long[0, short.length] == short + end + + # Override to provide command handling. + def execute + fail "Generic command has no actions" + end + + # Get all gem names from the command line. + def get_all_gem_names + args = options[:args] + + if args.nil? or args.empty? then + raise Gem::CommandLineError, + "Please specify at least one gem name (e.g. gem build GEMNAME)" + end + + gem_names = args.select { |arg| arg !~ /^-/ } + end + + # Get the single gem name from the command line. Fail if there is no gem + # name or if there is more than one gem name given. + def get_one_gem_name + args = options[:args] + + if args.nil? or args.empty? then + raise Gem::CommandLineError, + "Please specify a gem name on the command line (e.g. gem build GEMNAME)" + end + + if args.size > 1 then + raise Gem::CommandLineError, + "Too many gem names (#{args.join(', ')}); please specify only one" + end + + args.first + end + + # Get a single optional argument from the command line. If more than one + # argument is given, return only the first. Return nil if none are given. + def get_one_optional_argument + args = options[:args] || [] + args.first + end + + # Override to provide details of the arguments a command takes. + # It should return a left-justified string, one argument per line. + def arguments + "" + end + + # Override to display the default values of the command + # options. (similar to +arguments+, but displays the default + # values). + def defaults_str + "" + end + + # Override to display a longer description of what this command does. + def description + nil + end + + # Override to display the usage for an individual gem command. + def usage + program_name + end + + # Display the help message for the command. + def show_help + parser.program_name = usage + say parser + end + + # Invoke the command with the given list of arguments. + def invoke(*args) + handle_options(args) + if options[:help] + show_help + elsif @when_invoked + @when_invoked.call(options) + else + execute + end + end + + # Call the given block when invoked. + # + # Normal command invocations just executes the +execute+ method of + # the command. Specifying an invocation block allows the test + # methods to override the normal action of a command to determine + # that it has been invoked correctly. + def when_invoked(&block) + @when_invoked = block + end + + # Add a command-line option and handler to the command. + # + # See OptionParser#make_switch for an explanation of +opts+. + # + # +handler+ will be called with two values, the value of the argument and + # the options hash. + def add_option(*opts, &handler) # :yields: value, options + group_name = Symbol === opts.first ? opts.shift : :options + + @option_groups[group_name] << [opts, handler] + end + + # Remove previously defined command-line argument +name+. + def remove_option(name) + @option_groups.each do |_, option_list| + option_list.reject! { |args, _| args.any? { |x| x =~ /^#{name}/ } } + end + end + + # Merge a set of command options with the set of default options + # (without modifying the default option hash). + def merge_options(new_options) + @options = @defaults.clone + new_options.each do |k,v| @options[k] = v end + end + + # True if the command handles the given argument list. + def handles?(args) + begin + parser.parse!(args.dup) + return true + rescue + return false + end + end + + # Handle the given list of arguments by parsing them and recording + # the results. + def handle_options(args) + args = add_extra_args(args) + @options = @defaults.clone + parser.parse!(args) + @options[:args] = args + end + + def add_extra_args(args) + result = [] + s_extra = Command.specific_extra_args(@command) + extra = Command.extra_args + s_extra + while ! extra.empty? + ex = [] + ex << extra.shift + ex << extra.shift if extra.first.to_s =~ /^[^-]/ + result << ex if handles?(ex) + end + result.flatten! + result.concat(args) + result + end + + private + + # Create on demand parser. + def parser + create_option_parser if @parser.nil? + @parser + end + + def create_option_parser + @parser = OptionParser.new + + @parser.separator("") + regular_options = @option_groups.delete :options + + configure_options "", regular_options + + @option_groups.sort_by { |n,_| n.to_s }.each do |group_name, option_list| + configure_options group_name, option_list + end + + configure_options "Common", Command.common_options + + @parser.separator("") + unless arguments.empty? + @parser.separator(" Arguments:") + arguments.split(/\n/).each do |arg_desc| + @parser.separator(" #{arg_desc}") + end + @parser.separator("") + end + + @parser.separator(" Summary:") + wrap(@summary, 80 - 4).split("\n").each do |line| + @parser.separator(" #{line.strip}") + end + + if description then + formatted = description.split("\n\n").map do |chunk| + wrap(chunk, 80 - 4) + end.join("\n") + + @parser.separator "" + @parser.separator " Description:" + formatted.split("\n").each do |line| + @parser.separator " #{line.rstrip}" + end + end + + unless defaults_str.empty? + @parser.separator("") + @parser.separator(" Defaults:") + defaults_str.split(/\n/).each do |line| + @parser.separator(" #{line}") + end + end + end + + def configure_options(header, option_list) + return if option_list.nil? or option_list.empty? + + header = header.to_s.empty? ? '' : "#{header} " + @parser.separator " #{header}Options:" + + option_list.each do |args, handler| + dashes = args.select { |arg| arg =~ /^-/ } + @parser.on(*args) do |value| + handler.call(value, @options) + end + end + + @parser.separator '' + end + + # Wraps +text+ to +width+ + def wrap(text, width) + text.gsub(/(.{1,#{width}})( +|$\n?)|(.{1,#{width}})/, "\\1\\3\n") + end + + ################################################################## + # Class methods for Command. + class << self + def common_options + @common_options ||= [] + end + + def add_common_option(*args, &handler) + Gem::Command.common_options << [args, handler] + end + + def extra_args + @extra_args ||= [] + end + + def extra_args=(value) + case value + when Array + @extra_args = value + when String + @extra_args = value.split + end + end + + # Return an array of extra arguments for the command. The extra + # arguments come from the gem configuration file read at program + # startup. + def specific_extra_args(cmd) + specific_extra_args_hash[cmd] + end + + # Add a list of extra arguments for the given command. +args+ + # may be an array or a string to be split on white space. + def add_specific_extra_args(cmd,args) + args = args.split(/\s+/) if args.kind_of? String + specific_extra_args_hash[cmd] = args + end + + # Accessor for the specific extra args hash (self initializing). + def specific_extra_args_hash + @specific_extra_args_hash ||= Hash.new do |h,k| + h[k] = Array.new + end + end + end + + # ---------------------------------------------------------------- + # Add the options common to all commands. + + add_common_option('-h', '--help', + 'Get help on this command') do + |value, options| + options[:help] = true + end + + add_common_option('-V', '--[no-]verbose', + 'Set the verbose level of output') do |value, options| + # Set us to "really verbose" so the progess meter works + if Gem.configuration.verbose and value then + Gem.configuration.verbose = 1 + else + Gem.configuration.verbose = value + end + end + + add_common_option('-q', '--quiet', 'Silence commands') do |value, options| + Gem.configuration.verbose = false + end + + # Backtrace and config-file are added so they show up in the help + # commands. Both options are actually handled before the other + # options get parsed. + + add_common_option('--config-file FILE', + "Use this config file instead of default") do + end + + add_common_option('--backtrace', + 'Show stack backtrace on errors') do + end + + add_common_option('--debug', + 'Turn on Ruby debugging') do + end + + # :stopdoc: + HELP = %{ + RubyGems is a sophisticated package manager for Ruby. This is a + basic help message containing pointers to more information. + + Usage: + gem -h/--help + gem -v/--version + gem command [arguments...] [options...] + + Examples: + gem install rake + gem list --local + gem build package.gemspec + gem help install + + Further help: + gem help commands list all 'gem' commands + gem help examples show some examples of usage + gem help platforms show information about platforms + gem help show help on COMMAND + (e.g. 'gem help install') + Further information: + http://rubygems.rubyforge.org + }.gsub(/^ /, "") + + # :startdoc: + + end # class + + # This is where Commands will be placed in the namespace + module Commands; end + +end diff --git a/lib/rubygems/command_manager.rb b/lib/rubygems/command_manager.rb new file mode 100644 index 0000000000..a80c821c5c --- /dev/null +++ b/lib/rubygems/command_manager.rb @@ -0,0 +1,144 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'timeout' +require 'rubygems/command' +require 'rubygems/user_interaction' + +module Gem + + #################################################################### + # The command manager registers and installs all the individual + # sub-commands supported by the gem command. + class CommandManager + include UserInteraction + + # Return the authoratative instance of the command manager. + def self.instance + @command_manager ||= CommandManager.new + end + + # Register all the subcommands supported by the gem command. + def initialize + @commands = {} + register_command :build + register_command :cert + register_command :check + register_command :cleanup + register_command :contents + register_command :dependency + register_command :environment + register_command :fetch + register_command :generate_index + register_command :help + register_command :install + register_command :list + register_command :lock + register_command :mirror + register_command :outdated + register_command :pristine + register_command :query + register_command :rdoc + register_command :search + register_command :server + register_command :sources + register_command :specification + register_command :uninstall + register_command :unpack + register_command :update + register_command :which + end + + # Register the command object. + def register_command(command_obj) + @commands[command_obj] = false + end + + # Return the registered command from the command name. + def [](command_name) + command_name = command_name.intern + return nil if @commands[command_name].nil? + @commands[command_name] ||= load_and_instantiate(command_name) + end + + # Return a list of all command names (as strings). + def command_names + @commands.keys.collect {|key| key.to_s}.sort + end + + # Run the config specificed by +args+. + def run(args) + process_args(args) + rescue StandardError, Timeout::Error => ex + alert_error "While executing gem ... (#{ex.class})\n #{ex.to_s}" + ui.errs.puts "\t#{ex.backtrace.join "\n\t"}" if + Gem.configuration.backtrace + terminate_interaction(1) + rescue Interrupt + alert_error "Interrupted" + terminate_interaction(1) + end + + def process_args(args) + args = args.to_str.split(/\s+/) if args.respond_to?(:to_str) + if args.size == 0 + say Gem::Command::HELP + terminate_interaction(1) + end + case args[0] + when '-h', '--help' + say Gem::Command::HELP + terminate_interaction(0) + when '-v', '--version' + say Gem::RubyGemsPackageVersion + terminate_interaction(0) + when /^-/ + alert_error "Invalid option: #{args[0]}. See 'gem --help'." + terminate_interaction(1) + else + cmd_name = args.shift.downcase + cmd = find_command(cmd_name) + cmd.invoke(*args) + end + end + + def find_command(cmd_name) + possibilities = find_command_possibilities(cmd_name) + if possibilities.size > 1 + raise "Ambiguous command #{cmd_name} matches [#{possibilities.join(', ')}]" + end + if possibilities.size < 1 + raise "Unknown command #{cmd_name}" + end + + self[possibilities.first] + end + + def find_command_possibilities(cmd_name) + len = cmd_name.length + self.command_names.select { |n| cmd_name == n[0,len] } + end + + private + def load_and_instantiate(command_name) + command_name = command_name.to_s + retried = false + + begin + const_name = command_name.capitalize.gsub(/_(.)/) { $1.upcase } + Gem::Commands.const_get("#{const_name}Command").new + rescue NameError + if retried then + raise + else + retried = true + require "rubygems/commands/#{command_name}_command" + retry + end + end + end + end +end diff --git a/lib/rubygems/commands/build_command.rb b/lib/rubygems/commands/build_command.rb new file mode 100644 index 0000000000..c2e1abc92f --- /dev/null +++ b/lib/rubygems/commands/build_command.rb @@ -0,0 +1,53 @@ +require 'rubygems/command' +require 'rubygems/builder' + +class Gem::Commands::BuildCommand < Gem::Command + + def initialize + super('build', 'Build a gem from a gemspec') + end + + def arguments # :nodoc: + "GEMSPEC_FILE gemspec file name to build a gem for" + end + + def usage # :nodoc: + "#{program_name} GEMSPEC_FILE" + end + + def execute + gemspec = get_one_gem_name + if File.exist?(gemspec) + specs = load_gemspecs(gemspec) + specs.each do |spec| + Gem::Builder.new(spec).build + end + else + alert_error "Gemspec file not found: #{gemspec}" + end + end + + def load_gemspecs(filename) + if yaml?(filename) + result = [] + open(filename) do |f| + begin + while not f.eof? and spec = Gem::Specification.from_yaml(f) + result << spec + end + rescue Gem::EndOfYAMLException => e + # OK + end + end + else + result = [Gem::Specification.load(filename)] + end + result + end + + def yaml?(filename) + line = open(filename) { |f| line = f.gets } + result = line =~ %r{^--- *!ruby/object:Gem::Specification} + result + end +end diff --git a/lib/rubygems/commands/cert_command.rb b/lib/rubygems/commands/cert_command.rb new file mode 100644 index 0000000000..2c32099254 --- /dev/null +++ b/lib/rubygems/commands/cert_command.rb @@ -0,0 +1,86 @@ +require 'rubygems/command' +require 'rubygems/security' + +class Gem::Commands::CertCommand < Gem::Command + + def initialize + super 'cert', 'Manage RubyGems certificates and signing settings' + + add_option('-a', '--add CERT', + 'Add a trusted certificate.') do |value, options| + cert = OpenSSL::X509::Certificate.new(File.read(value)) + Gem::Security.add_trusted_cert(cert) + say "Added '#{cert.subject.to_s}'" + end + + add_option('-l', '--list', + 'List trusted certificates.') do |value, options| + glob_str = File::join(Gem::Security::OPT[:trust_dir], '*.pem') + Dir::glob(glob_str) do |path| + begin + cert = OpenSSL::X509::Certificate.new(File.read(path)) + # this could proably be formatted more gracefully + say cert.subject.to_s + rescue OpenSSL::X509::CertificateError + next + end + end + end + + add_option('-r', '--remove STRING', + 'Remove trusted certificates containing', + 'STRING.') do |value, options| + trust_dir = Gem::Security::OPT[:trust_dir] + glob_str = File::join(trust_dir, '*.pem') + + Dir::glob(glob_str) do |path| + begin + cert = OpenSSL::X509::Certificate.new(File.read(path)) + if cert.subject.to_s.downcase.index(value) + say "Removed '#{cert.subject.to_s}'" + File.unlink(path) + end + rescue OpenSSL::X509::CertificateError + next + end + end + end + + add_option('-b', '--build EMAIL_ADDR', + 'Build private key and self-signed', + 'certificate for EMAIL_ADDR.') do |value, options| + vals = Gem::Security.build_self_signed_cert(value) + File.chmod 0600, vals[:key_path] + say "Public Cert: #{vals[:cert_path]}" + say "Private Key: #{vals[:key_path]}" + say "Don't forget to move the key file to somewhere private..." + end + + add_option('-C', '--certificate CERT', + 'Certificate for --sign command.') do |value, options| + cert = OpenSSL::X509::Certificate.new(File.read(value)) + Gem::Security::OPT[:issuer_cert] = cert + end + + add_option('-K', '--private-key KEY', + 'Private key for --sign command.') do |value, options| + key = OpenSSL::PKey::RSA.new(File.read(value)) + Gem::Security::OPT[:issuer_key] = key + end + + add_option('-s', '--sign NEWCERT', + 'Sign a certificate with my key and', + 'certificate.') do |value, options| + cert = OpenSSL::X509::Certificate.new(File.read(value)) + my_cert = Gem::Security::OPT[:issuer_cert] + my_key = Gem::Security::OPT[:issuer_key] + cert = Gem::Security.sign_cert(cert, my_key, my_cert) + File.open(value, 'wb') { |file| file.write(cert.to_pem) } + end + end + + def execute + end + +end + diff --git a/lib/rubygems/commands/check_command.rb b/lib/rubygems/commands/check_command.rb new file mode 100644 index 0000000000..ca5e14b12d --- /dev/null +++ b/lib/rubygems/commands/check_command.rb @@ -0,0 +1,74 @@ +require 'rubygems/command' +require 'rubygems/version_option' +require 'rubygems/validator' + +class Gem::Commands::CheckCommand < Gem::Command + + include Gem::VersionOption + + def initialize + super 'check', 'Check installed gems', + :verify => false, :alien => false + + add_option( '--verify FILE', + 'Verify gem file against its internal', + 'checksum') do |value, options| + options[:verify] = value + end + + add_option('-a', '--alien', "Report 'unmanaged' or rogue files in the", + "gem repository") do |value, options| + options[:alien] = true + end + + add_option('-t', '--test', "Run unit tests for gem") do |value, options| + options[:test] = true + end + + add_version_option 'run tests for' + end + + def execute + if options[:test] + version = options[:version] || Gem::Requirement.default + gem_spec = Gem::SourceIndex.from_installed_gems.search(get_one_gem_name, version).first + Gem::Validator.new.unit_test(gem_spec) + end + + if options[:alien] + say "Performing the 'alien' operation" + Gem::Validator.new.alien.each do |key, val| + if(val.size > 0) + say "#{key} has #{val.size} problems" + val.each do |error_entry| + say "\t#{error_entry.path}:" + say "\t#{error_entry.problem}" + say + end + else + say "#{key} is error-free" + end + say + end + end + + if options[:verify] + gem_name = options[:verify] + unless gem_name + alert_error "Must specify a .gem file with --verify NAME" + return + end + unless File.exist?(gem_name) + alert_error "Unknown file: #{gem_name}." + return + end + say "Verifying gem: '#{gem_name}'" + begin + Gem::Validator.new.verify_gem_file(gem_name) + rescue Exception => e + alert_error "#{gem_name} is invalid." + end + end + end + +end diff --git a/lib/rubygems/commands/cleanup_command.rb b/lib/rubygems/commands/cleanup_command.rb new file mode 100644 index 0000000000..f6deac9829 --- /dev/null +++ b/lib/rubygems/commands/cleanup_command.rb @@ -0,0 +1,93 @@ +require 'rubygems/command' +require 'rubygems/source_index' +require 'rubygems/dependency_list' + +module Gem + module Commands + class CleanupCommand < Command + def initialize + super( + 'cleanup', + 'Clean up old versions of installed gems in the local repository', + { + :force => false, + :test => false, + :install_dir => Gem.dir + }) + add_option('-d', '--dryrun', "") do |value, options| + options[:dryrun] = true + end + end + + def arguments # :nodoc: + "GEMNAME name of gem to cleanup" + end + + def defaults_str # :nodoc: + "--no-dryrun" + end + + def usage # :nodoc: + "#{program_name} [GEMNAME ...]" + end + + def execute + say "Cleaning up installed gems..." + srcindex = Gem::SourceIndex.from_installed_gems + primary_gems = {} + + srcindex.each do |name, spec| + if primary_gems[spec.name].nil? or primary_gems[spec.name].version < spec.version + primary_gems[spec.name] = spec + end + end + + gems_to_cleanup = [] + + unless options[:args].empty? then + options[:args].each do |gem_name| + specs = Gem.cache.search(/^#{gem_name}$/i) + specs.each do |spec| + gems_to_cleanup << spec + end + end + else + srcindex.each do |name, spec| + gems_to_cleanup << spec + end + end + + gems_to_cleanup = gems_to_cleanup.select { |spec| + primary_gems[spec.name].version != spec.version + } + + uninstall_command = Gem::CommandManager.instance['uninstall'] + deplist = DependencyList.new + gems_to_cleanup.uniq.each do |spec| deplist.add(spec) end + + deplist.dependency_order.each do |spec| + if options[:dryrun] then + say "Dry Run Mode: Would uninstall #{spec.full_name}" + else + say "Attempting uninstall on #{spec.full_name}" + + options[:args] = [spec.name] + options[:version] = "= #{spec.version}" + options[:executables] = true + + uninstall_command.merge_options(options) + + begin + uninstall_command.execute + rescue Gem::DependencyRemovalException => ex + say "Unable to uninstall #{spec.full_name} ... continuing with remaining gems" + end + end + end + + say "Clean Up Complete" + end + end + + end +end diff --git a/lib/rubygems/commands/contents_command.rb b/lib/rubygems/commands/contents_command.rb new file mode 100644 index 0000000000..5060403fd8 --- /dev/null +++ b/lib/rubygems/commands/contents_command.rb @@ -0,0 +1,74 @@ +require 'rubygems/command' +require 'rubygems/version_option' + +class Gem::Commands::ContentsCommand < Gem::Command + + include Gem::VersionOption + + def initialize + super 'contents', 'Display the contents of the installed gems', + :specdirs => [], :lib_only => false + + add_version_option + + add_option('-s', '--spec-dir a,b,c', Array, + "Search for gems under specific paths") do |spec_dirs, options| + options[:specdirs] = spec_dirs + end + + add_option('-l', '--[no-]lib-only', + "Only return files in the Gem's lib_dirs") do |lib_only, options| + options[:lib_only] = lib_only + end + end + + def arguments # :nodoc: + "GEMNAME name of gem to list contents for" + end + + def defaults_str # :nodoc: + "--no-lib-only" + end + + def usage # :nodoc: + "#{program_name} GEMNAME" + end + + def execute + version = options[:version] || Gem::Requirement.default + gem = get_one_gem_name + + s = options[:specdirs].map do |i| + [i, File.join(i, "specifications")] + end.flatten + + path_kind = if s.empty? then + s = Gem::SourceIndex.installed_spec_directories + "default gem paths" + else + "specified path" + end + + si = Gem::SourceIndex.from_gems_in(*s) + + gem_spec = si.search(/\A#{gem}\z/, version).last + + unless gem_spec then + say "Unable to find gem '#{gem}' in #{path_kind}" + + if Gem.configuration.verbose then + say "\nDirectories searched:" + s.each { |dir| say dir } + end + + terminate_interaction + end + + files = options[:lib_only] ? gem_spec.lib_files : gem_spec.files + files.each do |f| + say File.join(gem_spec.full_gem_path, f) + end + end + +end + diff --git a/lib/rubygems/commands/dependency_command.rb b/lib/rubygems/commands/dependency_command.rb new file mode 100644 index 0000000000..1a43505d7c --- /dev/null +++ b/lib/rubygems/commands/dependency_command.rb @@ -0,0 +1,150 @@ +require 'rubygems/command' +require 'rubygems/local_remote_options' +require 'rubygems/version_option' +require 'rubygems/source_info_cache' + +class Gem::Commands::DependencyCommand < Gem::Command + + include Gem::LocalRemoteOptions + include Gem::VersionOption + + def initialize + super 'dependency', + 'Show the dependencies of an installed gem', + :version => Gem::Requirement.default, :domain => :local + + add_version_option + add_platform_option + + add_option('-R', '--[no-]reverse-dependencies', + 'Include reverse dependencies in the output') do + |value, options| + options[:reverse_dependencies] = value + end + + add_option('-p', '--pipe', + "Pipe Format (name --version ver)") do |value, options| + options[:pipe_format] = value + end + + add_local_remote_options + end + + def arguments # :nodoc: + "GEMNAME name of gem to show dependencies for" + end + + def defaults_str # :nodoc: + "--local --version '#{Gem::Requirement.default}' --no-reverse-dependencies" + end + + def usage # :nodoc: + "#{program_name} GEMNAME" + end + + def execute + options[:args] << '.' if options[:args].empty? + specs = {} + + source_indexes = [] + + if local? then + source_indexes << Gem::SourceIndex.from_installed_gems + end + + if remote? then + Gem::SourceInfoCache.cache_data.map do |_, sice| + source_indexes << sice.source_index + end + end + + options[:args].each do |name| + new_specs = nil + source_indexes.each do |source_index| + new_specs = find_gems(name, source_index) + end + + say "No match found for #{name} (#{options[:version]})" if + new_specs.empty? + + specs = specs.merge new_specs + end + + terminate_interaction 1 if specs.empty? + + reverse = Hash.new { |h, k| h[k] = [] } + + if options[:reverse_dependencies] then + specs.values.each do |source_index, spec| + reverse[spec.full_name] = find_reverse_dependencies spec, source_index + end + end + + if options[:pipe_format] then + specs.values.sort_by { |_, spec| spec }.each do |_, spec| + unless spec.dependencies.empty? + spec.dependencies.each do |dep| + say "#{dep.name} --version '#{dep.version_requirements}'" + end + end + end + else + response = '' + + specs.values.sort_by { |_, spec| spec }.each do |_, spec| + response << print_dependencies(spec) + unless reverse[spec.full_name].empty? then + response << " Used by\n" + reverse[spec.full_name].each do |sp, dep| + response << " #{sp} (#{dep})\n" + end + end + response << "\n" + end + + say response + end + end + + def print_dependencies(spec, level = 0) + response = '' + response << ' ' * level + "Gem #{spec.full_name}\n" + unless spec.dependencies.empty? then + spec.dependencies.each do |dep| + response << ' ' * level + " #{dep}\n" + end + end + response + end + + # Retuns list of [specification, dep] that are satisfied by spec. + def find_reverse_dependencies(spec, source_index) + result = [] + + source_index.each do |name, sp| + sp.dependencies.each do |dep| + dep = Gem::Dependency.new(*dep) unless Gem::Dependency === dep + + if spec.name == dep.name and + dep.version_requirements.satisfied_by?(spec.version) then + result << [sp.full_name, dep] + end + end + end + + result + end + + def find_gems(name, source_index) + specs = {} + + spec_list = source_index.search name, options[:version] + + spec_list.each do |spec| + specs[spec.full_name] = [source_index, spec] + end + + specs + end +end + diff --git a/lib/rubygems/commands/environment_command.rb b/lib/rubygems/commands/environment_command.rb new file mode 100644 index 0000000000..337d74893b --- /dev/null +++ b/lib/rubygems/commands/environment_command.rb @@ -0,0 +1,80 @@ +require 'rubygems/command' + +class Gem::Commands::EnvironmentCommand < Gem::Command + + def initialize + super 'environment', 'Display information about the RubyGems environment' + end + + def arguments # :nodoc: + args = <<-EOF + packageversion display the package version + gemdir display the path where gems are installed + gempath display path used to search for gems + version display the gem format version + remotesources display the remote gem servers + display everything + EOF + return args.gsub(/^\s+/, '') + end + + def usage # :nodoc: + "#{program_name} [arg]" + end + + def execute + out = '' + arg = options[:args][0] + if begins?("packageversion", arg) then + out << Gem::RubyGemsPackageVersion + elsif begins?("version", arg) then + out << Gem::RubyGemsVersion + elsif begins?("gemdir", arg) then + out << Gem.dir + elsif begins?("gempath", arg) then + out << Gem.path.join("\n") + elsif begins?("remotesources", arg) then + out << Gem.sources.join("\n") + elsif arg then + fail Gem::CommandLineError, "Unknown enviroment option [#{arg}]" + else + out = "RubyGems Environment:\n" + + out << " - RUBYGEMS VERSION: #{Gem::RubyGemsVersion} (#{Gem::RubyGemsPackageVersion})\n" + + out << " - RUBY VERSION: #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}" + out << " patchlevel #{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL + out << ") [#{RUBY_PLATFORM}]\n" + + out << " - INSTALLATION DIRECTORY: #{Gem.dir}\n" + + out << " - RUBYGEMS PREFIX: #{Gem.prefix}\n" unless Gem.prefix.nil? + + out << " - RUBY EXECUTABLE: #{Gem.ruby}\n" + + out << " - RUBYGEMS PLATFORMS:\n" + Gem.platforms.each do |platform| + out << " - #{platform}\n" + end + + out << " - GEM PATHS:\n" + Gem.path.each do |p| + out << " - #{p}\n" + end + + out << " - GEM CONFIGURATION:\n" + Gem.configuration.each do |name, value| + out << " - #{name.inspect} => #{value.inspect}\n" + end + + out << " - REMOTE SOURCES:\n" + Gem.sources.each do |s| + out << " - #{s}\n" + end + end + say out + true + end + +end + diff --git a/lib/rubygems/commands/fetch_command.rb b/lib/rubygems/commands/fetch_command.rb new file mode 100644 index 0000000000..7db365eba0 --- /dev/null +++ b/lib/rubygems/commands/fetch_command.rb @@ -0,0 +1,62 @@ +require 'rubygems/command' +require 'rubygems/local_remote_options' +require 'rubygems/version_option' +require 'rubygems/source_info_cache' + +class Gem::Commands::FetchCommand < Gem::Command + + include Gem::LocalRemoteOptions + include Gem::VersionOption + + def initialize + super 'fetch', 'Download a gem and place it in the current directory' + + add_bulk_threshold_option + add_proxy_option + add_source_option + + add_version_option + add_platform_option + end + + def arguments # :nodoc: + 'GEMNAME name of gem to download' + end + + def defaults_str # :nodoc: + "--version '#{Gem::Requirement.default}'" + end + + def usage # :nodoc: + "#{program_name} GEMNAME [GEMNAME ...]" + end + + def execute + version = options[:version] || Gem::Requirement.default + + gem_names = get_all_gem_names + + gem_names.each do |gem_name| + dep = Gem::Dependency.new gem_name, version + specs_and_sources = Gem::SourceInfoCache.search_with_source dep, true + + specs_and_sources.sort_by { |spec,| spec.version } + + spec, source_uri = specs_and_sources.last + + gem_file = "#{spec.full_name}.gem" + + gem_path = File.join source_uri, 'gems', gem_file + + gem = Gem::RemoteFetcher.fetcher.fetch_path gem_path + + File.open gem_file, 'wb' do |fp| + fp.write gem + end + + say "Downloaded #{gem_file}" + end + end + +end + diff --git a/lib/rubygems/commands/generate_index_command.rb b/lib/rubygems/commands/generate_index_command.rb new file mode 100644 index 0000000000..1bd87569ed --- /dev/null +++ b/lib/rubygems/commands/generate_index_command.rb @@ -0,0 +1,57 @@ +require 'rubygems/command' +require 'rubygems/indexer' + +class Gem::Commands::GenerateIndexCommand < Gem::Command + + def initialize + super 'generate_index', + 'Generates the index files for a gem server directory', + :directory => '.' + + add_option '-d', '--directory=DIRNAME', + 'repository base dir containing gems subdir' do |dir, options| + options[:directory] = File.expand_path dir + end + end + + def defaults_str # :nodoc: + "--directory ." + end + + def description # :nodoc: + <<-EOF +The generate_index command creates a set of indexes for serving gems +statically. The command expects a 'gems' directory under the path given to +the --directory option. When done, it will generate a set of files like this: + + gems/ # .gem files you want to index + quick/index + quick/index.rz # quick index manifest + quick/.gemspec.rz # legacy YAML quick index file + quick/Marshal./.gemspec.rz # Marshal quick index file + Marshal. + Marshal..Z # Marshal full index + yaml + yaml.Z # legacy YAML full index + +The .Z and .rz extension files are compressed with the inflate algorithm. The +Marshal version number comes from ruby's Marshal::MAJOR_VERSION and +Marshal::MINOR_VERSION constants. It is used to ensure compatibility. The +yaml indexes exist for legacy RubyGems clients and fallback in case of Marshal +version changes. + EOF + end + + def execute + if not File.exist?(options[:directory]) or + not File.directory?(options[:directory]) then + alert_error "unknown directory name #{directory}." + terminate_interaction 1 + else + indexer = Gem::Indexer.new options[:directory] + indexer.generate_index + end + end + +end + diff --git a/lib/rubygems/commands/help_command.rb b/lib/rubygems/commands/help_command.rb new file mode 100644 index 0000000000..05ea3f7a71 --- /dev/null +++ b/lib/rubygems/commands/help_command.rb @@ -0,0 +1,172 @@ +require 'rubygems/command' + +class Gem::Commands::HelpCommand < Gem::Command + + # :stopdoc: + EXAMPLES = <<-EOF +Some examples of 'gem' usage. + +* Install 'rake', either from local directory or remote server: + + gem install rake + +* Install 'rake', only from remote server: + + gem install rake --remote + +* Install 'rake' from remote server, and run unit tests, + and generate RDocs: + + gem install --remote rake --test --rdoc --ri + +* Install 'rake', but only version 0.3.1, even if dependencies + are not met, and into a specific directory: + + gem install rake --version 0.3.1 --force --install-dir $HOME/.gems + +* List local gems whose name begins with 'D': + + gem list D + +* List local and remote gems whose name contains 'log': + + gem search log --both + +* List only remote gems whose name contains 'log': + + gem search log --remote + +* Uninstall 'rake': + + gem uninstall rake + +* Create a gem: + + See http://rubygems.rubyforge.org/wiki/wiki.pl?CreateAGemInTenMinutes + +* See information about RubyGems: + + gem environment + +* Update all gems on your system: + + gem update + EOF + + PLATFORMS = <<-'EOF' +RubyGems platforms are composed of three parts, a CPU, an OS, and a +version. These values are taken from values in rbconfig.rb. You can view +your current platform by running `gem environment`. + +RubyGems matches platforms as follows: + + * The CPU must match exactly, unless one of the platforms has + "universal" as the CPU. + * The OS must match exactly. + * The versions must match exactly unless one of the versions is nil. + +For commands that install, uninstall and list gems, you can override what +RubyGems thinks your platform is with the --platform option. The platform +you pass must match "#{cpu}-#{os}" or "#{cpu}-#{os}-#{version}". On mswin +platforms, the version is the compiler version, not the OS version. (Ruby +compiled with VC6 uses "60" as the compiler version, VC8 uses "80".) + +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 + +When building platform gems, set the platform in the gem specification to +Gem::Platform::CURRENT. This will correctly mark the gem with your ruby's +platform. + EOF + # :startdoc: + + def initialize + super 'help', "Provide help on the 'gem' command" + end + + def arguments # :nodoc: + args = <<-EOF + commands List all 'gem' commands + examples Show examples of 'gem' usage + Show specific help for + EOF + return args.gsub(/^\s+/, '') + end + + def usage # :nodoc: + "#{program_name} ARGUMENT" + end + + def execute + command_manager = Gem::CommandManager.instance + arg = options[:args][0] + + if begins? "commands", arg then + out = [] + out << "GEM commands are:" + out << nil + + margin_width = 4 + + desc_width = command_manager.command_names.map { |n| n.size }.max + 4 + + summary_width = 80 - margin_width - desc_width + wrap_indent = ' ' * (margin_width + desc_width) + format = "#{' ' * margin_width}%-#{desc_width}s%s" + + command_manager.command_names.each do |cmd_name| + summary = command_manager[cmd_name].summary + summary = wrap(summary, summary_width).split "\n" + out << sprintf(format, cmd_name, summary.shift) + until summary.empty? do + out << "#{wrap_indent}#{summary.shift}" + end + end + + out << nil + out << "For help on a particular command, use 'gem help COMMAND'." + out << nil + out << "Commands may be abbreviated, so long as they are unambiguous." + out << "e.g. 'gem i rake' is short for 'gem install rake'." + + say out.join("\n") + + elsif begins? "options", arg then + say Gem::Command::HELP + + elsif begins? "examples", arg then + say EXAMPLES + + elsif begins? "platforms", arg then + say PLATFORMS + + elsif options[:help] then + command = command_manager[options[:help]] + if command + # help with provided command + command.invoke("--help") + else + alert_error "Unknown command #{options[:help]}. Try 'gem help commands'" + end + + elsif arg then + possibilities = command_manager.find_command_possibilities(arg.downcase) + if possibilities.size == 1 + command = command_manager[possibilities.first] + command.invoke("--help") + elsif possibilities.size > 1 + alert_warning "Ambiguous command #{arg} (#{possibilities.join(', ')})" + else + alert_warning "Unknown command #{arg}. Try gem help commands" + end + + else + say Gem::Command::HELP + end + end + +end + diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb new file mode 100644 index 0000000000..4c67c0487b --- /dev/null +++ b/lib/rubygems/commands/install_command.rb @@ -0,0 +1,125 @@ +require 'rubygems/command' +require 'rubygems/doc_manager' +require 'rubygems/install_update_options' +require 'rubygems/dependency_installer' +require 'rubygems/local_remote_options' +require 'rubygems/validator' +require 'rubygems/version_option' + +class Gem::Commands::InstallCommand < Gem::Command + + include Gem::VersionOption + include Gem::LocalRemoteOptions + include Gem::InstallUpdateOptions + + def initialize + defaults = Gem::DependencyInstaller::DEFAULT_OPTIONS.merge({ + :generate_rdoc => true, + :generate_ri => true, + :install_dir => Gem.dir, + :test => false, + :version => Gem::Requirement.default, + }) + + super 'install', 'Install a gem into the local repository', defaults + + add_install_update_options + add_local_remote_options + add_platform_option + add_version_option + end + + def arguments # :nodoc: + "GEMNAME name of gem to install" + end + + def defaults_str # :nodoc: + "--both --version '#{Gem::Requirement.default}' --rdoc --ri --no-force\n" \ + "--no-test --install-dir #{Gem.dir}" + end + + def usage # :nodoc: + "#{program_name} GEMNAME [GEMNAME ...] [options] -- --build-flags" + end + + def execute + if options[:include_dependencies] then + alert "`gem install -y` is now default and will be removed" + alert "use --ignore-dependencies to install only the gems you list" + end + + installed_gems = [] + + ENV['GEM_PATH'] = options[:install_dir] # HACK what does this do? + + install_options = { + :env_shebang => options[:env_shebang], + :domain => options[:domain], + :force => options[:force], + :ignore_dependencies => options[:ignore_dependencies], + :install_dir => options[:install_dir], + :security_policy => options[:security_policy], + :wrappers => options[:wrappers], + } + + get_all_gem_names.each do |gem_name| + begin + inst = Gem::DependencyInstaller.new gem_name, options[:version], + install_options + inst.install + + inst.installed_gems.each do |spec| + say "Successfully installed #{spec.full_name}" + end + + installed_gems.push(*inst.installed_gems) + rescue Gem::InstallError => e + alert_error "Error installing #{gem_name}:\n\t#{e.message}" + rescue Gem::GemNotFoundException => e + alert_error e.message +# rescue => e +# # TODO: Fix this handle to allow the error to propagate to +# # the top level handler. Examine the other errors as +# # well. This implementation here looks suspicious to me -- +# # JimWeirich (4/Jan/05) +# alert_error "Error installing gem #{gem_name}: #{e.message}" +# return + end + end + + unless installed_gems.empty? then + gems = installed_gems.length == 1 ? 'gem' : 'gems' + say "#{installed_gems.length} #{gems} installed" + end + + # NOTE: *All* of the RI documents must be generated first. + # For some reason, RI docs cannot be generated after any RDoc + # documents are generated. + + if options[:generate_ri] then + installed_gems.each do |gem| + Gem::DocManager.new(gem, options[:rdoc_args]).generate_ri + end + end + + if options[:generate_rdoc] then + installed_gems.each do |gem| + Gem::DocManager.new(gem, options[:rdoc_args]).generate_rdoc + end + end + + if options[:test] then + installed_gems.each do |spec| + gem_spec = Gem::SourceIndex.from_installed_gems.search(spec.name, spec.version.version).first + result = Gem::Validator.new.unit_test(gem_spec) + if result and not result.passed? + unless ask_yes_no("...keep Gem?", true) then + Gem::Uninstaller.new(spec.name, :version => spec.version.version).uninstall + end + end + end + end + end + +end + diff --git a/lib/rubygems/commands/list_command.rb b/lib/rubygems/commands/list_command.rb new file mode 100644 index 0000000000..e179ff57ee --- /dev/null +++ b/lib/rubygems/commands/list_command.rb @@ -0,0 +1,35 @@ +require 'rubygems/command' +require 'rubygems/commands/query_command' + +module Gem + module Commands + class ListCommand < QueryCommand + + def initialize + super( + 'list', + 'Display all gems whose name starts with STRING' + ) + remove_option('--name-matches') + end + + def arguments # :nodoc: + "STRING start of gem name to look for" + end + + def defaults_str # :nodoc: + "--local --no-details" + end + + def usage # :nodoc: + "#{program_name} [STRING]" + end + + def execute + string = get_one_optional_argument || '' + options[:name] = /^#{string}/i + super + end + end + end +end diff --git a/lib/rubygems/commands/lock_command.rb b/lib/rubygems/commands/lock_command.rb new file mode 100644 index 0000000000..3a3dcc0c6b --- /dev/null +++ b/lib/rubygems/commands/lock_command.rb @@ -0,0 +1,101 @@ +require 'rubygems/command' + +class Gem::Commands::LockCommand < Gem::Command + + def initialize + super 'lock', 'Generate a lockdown list of gems', + :strict => false + + add_option '-s', '--[no-]strict', + 'fail if unable to satisfy a dependency' do |strict, options| + options[:strict] = strict + end + end + + def arguments # :nodoc: + "GEMNAME name of gem to lock\nVERSION version of gem to lock" + end + + def defaults_str # :nodoc: + "--no-strict" + end + + def description # :nodoc: + <<-EOF +The lock command will generate a list of +gem+ statements that will lock down +the versions for the gem given in the command line. It will specify exact +versions in the requirements list to ensure that the gems loaded will always +be consistent. A full recursive search of all effected gems will be +generated. + +Example: + + gemlock rails-1.0.0 > lockdown.rb + +will produce in lockdown.rb: + + require "rubygems" + gem 'rails', '= 1.0.0' + gem 'rake', '= 0.7.0.1' + gem 'activesupport', '= 1.2.5' + gem 'activerecord', '= 1.13.2' + gem 'actionpack', '= 1.11.2' + gem 'actionmailer', '= 1.1.5' + gem 'actionwebservice', '= 1.0.0' + +Just load lockdown.rb from your application to ensure that the current +versions are loaded. Make sure that lockdown.rb is loaded *before* any +other require statements. + +Notice that rails 1.0.0 only requires that rake 0.6.2 or better be used. +Rake-0.7.0.1 is the most recent version installed that satisfies that, so we +lock it down to the exact version. + EOF + end + + def usage # :nodoc: + "#{program_name} GEMNAME-VERSION [GEMNAME-VERSION ...]" + end + + def complain(message) + if options.strict then + raise message + else + say "# #{message}" + end + end + + def execute + say 'require "rubygems"' + + locked = {} + + pending = options[:args] + + until pending.empty? do + full_name = pending.shift + + spec = Gem::SourceIndex.load_specification spec_path(full_name) + + say "gem '#{spec.name}', '= #{spec.version}'" unless locked[spec.name] + locked[spec.name] = true + + spec.dependencies.each do |dep| + next if locked[dep.name] + candidates = Gem.source_index.search dep.name, dep.requirement_list + + if candidates.empty? then + complain "Unable to satisfy '#{dep}' from currently installed gems." + else + pending << candidates.last.full_name + end + end + end + end + + def spec_path(gem_full_name) + File.join Gem.path, "specifications", "#{gem_full_name }.gemspec" + end + +end + diff --git a/lib/rubygems/commands/mirror_command.rb b/lib/rubygems/commands/mirror_command.rb new file mode 100644 index 0000000000..74f6970e9e --- /dev/null +++ b/lib/rubygems/commands/mirror_command.rb @@ -0,0 +1,105 @@ +require 'yaml' +require 'zlib' + +require 'rubygems/command' +require 'rubygems/gem_open_uri' + +class Gem::Commands::MirrorCommand < Gem::Command + + def initialize + super 'mirror', 'Mirror a gem repository' + end + + def description # :nodoc: + <<-EOF +The mirror command uses the ~/.gemmirrorrc config file to mirror remote gem +repositories to a local path. The config file is a YAML document that looks +like this: + + --- + - from: http://gems.example.com # source repository URI + to: /path/to/mirror # destination directory + +Multiple sources and destinations may be specified. + EOF + end + + def execute + config_file = File.join Gem.user_home, '.gemmirrorrc' + + raise "Config file #{config_file} not found" unless File.exist? config_file + + mirrors = YAML.load_file config_file + + raise "Invalid config file #{config_file}" unless mirrors.respond_to? :each + + mirrors.each do |mir| + raise "mirror missing 'from' field" unless mir.has_key? 'from' + raise "mirror missing 'to' field" unless mir.has_key? 'to' + + get_from = mir['from'] + save_to = File.expand_path mir['to'] + + raise "Directory not found: #{save_to}" unless File.exist? save_to + raise "Not a directory: #{save_to}" unless File.directory? save_to + + gems_dir = File.join save_to, "gems" + + if File.exist? gems_dir then + raise "Not a directory: #{gems_dir}" unless File.directory? gems_dir + else + Dir.mkdir gems_dir + end + + sourceindex_data = '' + + say "fetching: #{get_from}/Marshal.#{Gem.marshal_version}.Z" + + get_from = URI.parse get_from + + if get_from.scheme.nil? then + get_from = get_from.to_s + elsif get_from.scheme == 'file' then + get_from = get_from.to_s[5..-1] + end + + open File.join(get_from, "Marshal.#{Gem.marshal_version}.Z"), "rb" do |y| + sourceindex_data = Zlib::Inflate.inflate y.read + open File.join(save_to, "Marshal.#{Gem.marshal_version}"), "wb" do |out| + out.write sourceindex_data + end + end + + sourceindex = Marshal.load(sourceindex_data) + + progress = ui.progress_reporter sourceindex.size, + "Fetching #{sourceindex.size} gems" + sourceindex.each do |fullname, gem| + gem_file = "#{fullname}.gem" + gem_dest = File.join gems_dir, gem_file + + unless File.exist? gem_dest then + begin + open "#{get_from}/gems/#{gem_file}", "rb" do |g| + contents = g.read + open gem_dest, "wb" do |out| + out.write contents + end + end + rescue + old_gf = gem_file + gem_file = gem_file.downcase + retry if old_gf != gem_file + alert_error $! + end + end + + progress.updated gem_file + end + + progress.done + end + end + +end + diff --git a/lib/rubygems/commands/outdated_command.rb b/lib/rubygems/commands/outdated_command.rb new file mode 100644 index 0000000000..9c0062019b --- /dev/null +++ b/lib/rubygems/commands/outdated_command.rb @@ -0,0 +1,30 @@ +require 'rubygems/command' +require 'rubygems/local_remote_options' +require 'rubygems/source_info_cache' +require 'rubygems/version_option' + +class Gem::Commands::OutdatedCommand < Gem::Command + + include Gem::LocalRemoteOptions + include Gem::VersionOption + + def initialize + super 'outdated', 'Display all gems that need updates' + + add_local_remote_options + add_platform_option + end + + def execute + locals = Gem::SourceIndex.from_installed_gems + + locals.outdated.sort.each do |name| + local = locals.search(/^#{name}$/).last + remotes = Gem::SourceInfoCache.search_with_source(/^#{name}$/, true) + remote = remotes.last.first + say "#{local.name} (#{local.version} < #{remote.version})" + end + end + +end + diff --git a/lib/rubygems/commands/pristine_command.rb b/lib/rubygems/commands/pristine_command.rb new file mode 100644 index 0000000000..2900e7e739 --- /dev/null +++ b/lib/rubygems/commands/pristine_command.rb @@ -0,0 +1,133 @@ +require 'fileutils' +require 'rubygems/command' +require 'rubygems/format' +require 'rubygems/installer' +require 'rubygems/version_option' + +class Gem::Commands::PristineCommand < Gem::Command + + include Gem::VersionOption + + def initialize + super 'pristine', + 'Restores installed gems to pristine condition from files located in the gem cache', + :version => Gem::Requirement.default + + add_option('--all', + 'Restore all installed gems to pristine', + 'condition') do |value, options| + options[:all] = value + end + + add_version_option('restore to', 'pristine condition') + end + + def arguments # :nodoc: + "GEMNAME gem to restore to pristine condition (unless --all)" + end + + def defaults_str # :nodoc: + "--all" + end + + def description # :nodoc: + <<-EOF +The pristine command compares the installed gems with the contents of the +cached gem and restores any files that don't match the cached gem's copy. + +If you have made modifications to your installed gems, the pristine command +will revert them. After all the gem's files have been checked all bin stubs +for the gem are regenerated. + +If the cached gem cannot be found, you will need to use `gem install` to +revert the gem. + EOF + end + + def usage # :nodoc: + "#{program_name} [args]" + end + + def execute + gem_name = nil + + specs = if options[:all] then + Gem::SourceIndex.from_installed_gems.map do |name, spec| + spec + end + else + gem_name = get_one_gem_name + Gem::SourceIndex.from_installed_gems.search(gem_name, + options[:version]) + end + + if specs.empty? then + raise Gem::Exception, + "Failed to find gem #{gem_name} #{options[:version]}" + end + + install_dir = Gem.dir # TODO use installer option + + raise Gem::FilePermissionError.new(install_dir) unless + File.writable?(install_dir) + + say "Restoring gem(s) to pristine condition..." + + specs.each do |spec| + gem = Dir[File.join(Gem.dir, 'cache', "#{spec.full_name}.gem")].first + + if gem.nil? then + alert_error "Cached gem for #{spec.full_name} not found, use `gem install` to restore" + next + end + + # TODO use installer options + installer = Gem::Installer.new gem, :wrappers => true + + gem_file = File.join install_dir, "cache", "#{spec.full_name}.gem" + + security_policy = nil # TODO use installer option + + format = Gem::Format.from_file_by_path gem_file, security_policy + + target_directory = File.join(install_dir, "gems", format.spec.full_name) + target_directory.untaint + + pristine_files = format.file_entries.collect { |data| data[0]["path"] } + file_map = {} + + format.file_entries.each do |entry, file_data| + file_map[entry["path"]] = file_data + end + + Dir.chdir target_directory do + deployed_files = Dir.glob(File.join("**", "*")) + + Dir.glob(File.join("**", ".*")) + + pristine_files = pristine_files.map { |f| File.expand_path f } + deployed_files = deployed_files.map { |f| File.expand_path f } + + to_redeploy = (pristine_files - deployed_files) + to_redeploy = to_redeploy.map { |path| path.untaint} + + if to_redeploy.length > 0 then + say "Restoring #{to_redeploy.length} file#{to_redeploy.length == 1 ? "" : "s"} to #{spec.full_name}..." + + to_redeploy.each do |path| + say " #{path}" + FileUtils.mkdir_p File.dirname(path) + File.open(path, "wb") do |out| + out.write file_map[path] + end + end + else + say "#{spec.full_name} is in pristine condition" + end + end + + installer.generate_bin + end + end + +end + diff --git a/lib/rubygems/commands/query_command.rb b/lib/rubygems/commands/query_command.rb new file mode 100644 index 0000000000..581d4bb734 --- /dev/null +++ b/lib/rubygems/commands/query_command.rb @@ -0,0 +1,118 @@ +require 'rubygems/command' +require 'rubygems/local_remote_options' +require 'rubygems/source_info_cache' + +class Gem::Commands::QueryCommand < Gem::Command + + include Gem::LocalRemoteOptions + + def initialize(name = 'query', + summary = 'Query gem information in local or remote repositories') + super name, summary, + :name => /.*/, :domain => :local, :details => false, :versions => true + + add_option('-n', '--name-matches REGEXP', + 'Name of gem(s) to query on matches the', + 'provided REGEXP') do |value, options| + options[:name] = /#{value}/i + end + + add_option('-d', '--[no-]details', + 'Display detailed information of gem(s)') do |value, options| + options[:details] = value + end + + add_option( '--[no-]versions', + 'Display only gem names') do |value, options| + options[:versions] = value + options[:details] = false unless value + end + + add_local_remote_options + end + + def defaults_str # :nodoc: + "--local --name-matches '.*' --no-details --versions" + end + + def execute + name = options[:name] + + if local? then + say + say "*** LOCAL GEMS ***" + say + output_query_results Gem.cache.search(name) + end + + if remote? then + say + say "*** REMOTE GEMS ***" + say + output_query_results Gem::SourceInfoCache.search(name) + end + end + + private + + def output_query_results(gemspecs) + output = [] + gem_list_with_version = {} + + gemspecs.flatten.each do |gemspec| + gem_list_with_version[gemspec.name] ||= [] + gem_list_with_version[gemspec.name] << gemspec + end + + gem_list_with_version = gem_list_with_version.sort_by do |name, spec| + name.downcase + end + + gem_list_with_version.each do |gem_name, list_of_matching| + list_of_matching = list_of_matching.sort_by { |x| x.version.to_ints }.reverse + seen_versions = {} + + list_of_matching.delete_if do |item| + if seen_versions[item.version] then + true + else + seen_versions[item.version] = true + false + end + end + + entry = gem_name.dup + if options[:versions] then + entry << " (#{list_of_matching.map{|gem| gem.version.to_s}.join(", ")})" + end + + entry << "\n" << format_text(list_of_matching[0].summary, 68, 4) if + options[:details] + output << entry + end + + say output.join(options[:details] ? "\n\n" : "\n") + end + + ## + # Used for wrapping and indenting text + # + def format_text(text, wrap, indent=0) + result = [] + work = text.dup + + while work.length > wrap + if work =~ /^(.{0,#{wrap}})[ \n]/o then + result << $1 + work.slice!(0, $&.length) + else + result << work.slice!(0, wrap) + end + end + + result << work if work.length.nonzero? + result.join("\n").gsub(/^/, " " * indent) + end + +end + diff --git a/lib/rubygems/commands/rdoc_command.rb b/lib/rubygems/commands/rdoc_command.rb new file mode 100644 index 0000000000..f2e677c115 --- /dev/null +++ b/lib/rubygems/commands/rdoc_command.rb @@ -0,0 +1,78 @@ +require 'rubygems/command' +require 'rubygems/version_option' +require 'rubygems/doc_manager' + +module Gem + module Commands + class RdocCommand < Command + include VersionOption + + def initialize + super('rdoc', + 'Generates RDoc for pre-installed gems', + { + :version => Gem::Requirement.default, + :include_rdoc => true, + :include_ri => true, + }) + add_option('--all', + 'Generate RDoc/RI documentation for all', + 'installed gems') do |value, options| + options[:all] = value + end + add_option('--[no-]rdoc', + 'Include RDoc generated documents') do + |value, options| + options[:include_rdoc] = value + end + add_option('--[no-]ri', + 'Include RI generated documents' + ) do |value, options| + options[:include_ri] = value + end + add_version_option + end + + def arguments # :nodoc: + "GEMNAME gem to generate documentation for (unless --all)" + end + + def defaults_str # :nodoc: + "--version '#{Gem::Requirement.default}' --rdoc --ri" + end + + def usage # :nodoc: + "#{program_name} [args]" + end + + def execute + if options[:all] + specs = Gem::SourceIndex.from_installed_gems.collect { |name, spec| + spec + } + else + gem_name = get_one_gem_name + specs = Gem::SourceIndex.from_installed_gems.search( + gem_name, options[:version]) + end + + if specs.empty? + fail "Failed to find gem #{gem_name} to generate RDoc for #{options[:version]}" + end + if options[:include_ri] + specs.each do |spec| + Gem::DocManager.new(spec).generate_ri + end + end + if options[:include_rdoc] + specs.each do |spec| + Gem::DocManager.new(spec).generate_rdoc + end + end + + true + end + end + + end +end diff --git a/lib/rubygems/commands/search_command.rb b/lib/rubygems/commands/search_command.rb new file mode 100644 index 0000000000..96da19c0f7 --- /dev/null +++ b/lib/rubygems/commands/search_command.rb @@ -0,0 +1,37 @@ +require 'rubygems/command' +require 'rubygems/commands/query_command' + +module Gem + module Commands + + class SearchCommand < QueryCommand + + def initialize + super( + 'search', + 'Display all gems whose name contains STRING' + ) + remove_option('--name-matches') + end + + def arguments # :nodoc: + "STRING fragment of gem name to search for" + end + + def defaults_str # :nodoc: + "--local --no-details" + end + + def usage # :nodoc: + "#{program_name} [STRING]" + end + + def execute + string = get_one_optional_argument + options[:name] = /#{string}/i + super + end + end + + end +end diff --git a/lib/rubygems/commands/server_command.rb b/lib/rubygems/commands/server_command.rb new file mode 100644 index 0000000000..34e5e46fec --- /dev/null +++ b/lib/rubygems/commands/server_command.rb @@ -0,0 +1,48 @@ +require 'rubygems/command' +require 'rubygems/server' + +class Gem::Commands::ServerCommand < Gem::Command + + def initialize + super 'server', 'Documentation and gem repository HTTP server', + :port => 8808, :gemdir => Gem.dir, :daemon => false + + add_option '-p', '--port=PORT', + 'port to listen on' do |port, options| + options[:port] = port + end + + add_option '-d', '--dir=GEMDIR', + 'directory from which to serve gems' do |gemdir, options| + options[:gemdir] = gemdir + end + + add_option '--[no]-daemon', 'run as a daemon' do |daemon, options| + options[:daemon] = daemon + end + end + + def defaults_str # :nodoc: + "--port 8808 --dir #{Gem.dir} --no-daemon" + end + + def description # :nodoc: + <<-EOF +The server command starts up a web server that hosts the RDoc for your +installed gems and can operate as a server for installation of gems on other +machines. + +The cache files for installed gems must exist to use the server as a source +for gem installation. + +To install gems from a running server, use `gem install GEMNAME --source +http://gem_server_host:8808` + EOF + end + + def execute + Gem::Server.run options + end + +end + diff --git a/lib/rubygems/commands/sources_command.rb b/lib/rubygems/commands/sources_command.rb new file mode 100644 index 0000000000..def9c01a3f --- /dev/null +++ b/lib/rubygems/commands/sources_command.rb @@ -0,0 +1,115 @@ +require 'rubygems/command' +require 'rubygems/remote_fetcher' +require 'rubygems/source_info_cache' +require 'rubygems/source_info_cache_entry' + +class Gem::Commands::SourcesCommand < Gem::Command + + def initialize + super 'sources', + 'Manage the sources and cache file RubyGems uses to search for gems' + + add_option '-a', '--add SOURCE_URI', 'Add source' do |value, options| + options[:add] = value + end + + add_option '-l', '--list', 'List sources' do |value, options| + options[:list] = value + end + + add_option '-r', '--remove SOURCE_URI', 'Remove source' do |value, options| + options[:remove] = value + end + + add_option '-u', '--update', 'Update source cache' do |value, options| + options[:update] = value + end + + add_option '-c', '--clear-all', + 'Remove all sources (clear the cache)' do |value, options| + options[:clear_all] = value + end + end + + def defaults_str + '--list' + end + + def execute + options[:list] = !(options[:add] || options[:remove] || options[:clear_all] || options[:update]) + + if options[:clear_all] then + remove_cache_file("user", Gem::SourceInfoCache.user_cache_file) + remove_cache_file("system", Gem::SourceInfoCache.system_cache_file) + end + + if options[:add] then + source_uri = options[:add] + + sice = Gem::SourceInfoCacheEntry.new nil, nil + begin + sice.refresh source_uri + + Gem::SourceInfoCache.cache_data[source_uri] = sice + Gem::SourceInfoCache.cache.update + Gem::SourceInfoCache.cache.flush + + Gem.sources << source_uri + Gem.configuration.write + + say "#{source_uri} added to sources" + rescue URI::Error, ArgumentError + say "#{source_uri} is not a URI" + rescue Gem::RemoteFetcher::FetchError => e + say "Error fetching #{source_uri}:\n\t#{e.message}" + end + end + + if options[:update] then + Gem::SourceInfoCache.cache.refresh + Gem::SourceInfoCache.cache.flush + + say "source cache successfully updated" + end + + if options[:remove] then + source_uri = options[:remove] + + unless Gem.sources.include? source_uri then + say "source #{source_uri} not present in cache" + else + Gem::SourceInfoCache.cache_data.delete source_uri + Gem::SourceInfoCache.cache.update + Gem::SourceInfoCache.cache.flush + Gem.sources.delete source_uri + Gem.configuration.write + + say "#{source_uri} removed from sources" + end + end + + if options[:list] then + say "*** CURRENT SOURCES ***" + say + + Gem.sources.each do |source_uri| + say source_uri + end + end + end + + private + + def remove_cache_file(desc, fn) + FileUtils.rm_rf fn rescue nil + if ! File.exist?(fn) + say "*** Removed #{desc} source cache ***" + elsif ! File.writable?(fn) + say "*** Unable to remove #{desc} source cache (write protected) ***" + else + say "*** Unable to remove #{desc} source cache ***" + end + end + +end + diff --git a/lib/rubygems/commands/specification_command.rb b/lib/rubygems/commands/specification_command.rb new file mode 100644 index 0000000000..954b38ac37 --- /dev/null +++ b/lib/rubygems/commands/specification_command.rb @@ -0,0 +1,72 @@ +require 'yaml' +require 'rubygems/command' +require 'rubygems/local_remote_options' +require 'rubygems/version_option' +require 'rubygems/source_info_cache' + +class Gem::Commands::SpecificationCommand < Gem::Command + + include Gem::LocalRemoteOptions + include Gem::VersionOption + + def initialize + super 'specification', 'Display gem specification (in yaml)', + :domain => :local, :version => Gem::Requirement.default + + add_version_option('examine') + add_platform_option + + add_option('--all', 'Output specifications for all versions of', + 'the gem') do |value, options| + options[:all] = true + end + + add_local_remote_options + end + + def arguments # :nodoc: + "GEMFILE name of gem to show the gemspec for" + end + + def defaults_str # :nodoc: + "--local --version '#{Gem::Requirement.default}'" + end + + def usage # :nodoc: + "#{program_name} [GEMFILE]" + end + + def execute + specs = [] + gem = get_one_gem_name + + if local? then + source_index = Gem::SourceIndex.from_installed_gems + specs.push(*source_index.search(/\A#{gem}\z/, options[:version])) + end + + if remote? then + alert_warning "Remote information is not complete\n\n" + + Gem::SourceInfoCache.cache_data.each do |_,sice| + specs.push(*sice.source_index.search(gem, options[:version])) + end + end + + if specs.empty? then + alert_error "Unknown gem '#{gem}'" + terminate_interaction 1 + end + + output = lambda { |spec| say spec.to_yaml; say "\n" } + + if options[:all] then + specs.each(&output) + else + spec = specs.sort_by { |spec| spec.version }.last + output[spec] + end + end + +end + diff --git a/lib/rubygems/commands/uninstall_command.rb b/lib/rubygems/commands/uninstall_command.rb new file mode 100644 index 0000000000..7d2908836c --- /dev/null +++ b/lib/rubygems/commands/uninstall_command.rb @@ -0,0 +1,56 @@ +require 'rubygems/command' +require 'rubygems/version_option' +require 'rubygems/uninstaller' + +module Gem + module Commands + class UninstallCommand < Command + + include VersionOption + + def initialize + super 'uninstall', 'Uninstall gems from the local repository', + :version => Gem::Requirement.default + + add_option('-a', '--[no-]all', + 'Uninstall all matching versions' + ) do |value, options| + options[:all] = value + end + + add_option('-i', '--[no-]ignore-dependencies', + 'Ignore dependency requirements while', + 'uninstalling') do |value, options| + options[:ignore] = value + end + + add_option('-x', '--[no-]executables', + 'Uninstall applicable executables without', + 'confirmation') do |value, options| + options[:executables] = value + end + + add_version_option + add_platform_option + end + + def arguments # :nodoc: + "GEMNAME name of gem to uninstall" + end + + def defaults_str # :nodoc: + "--version '#{Gem::Requirement.default}' --no-force" + end + + def usage # :nodoc: + "#{program_name} GEMNAME [GEMNAME ...]" + end + + def execute + get_all_gem_names.each do |gem_name| + Gem::Uninstaller.new(gem_name, options).uninstall + end + end + end + end +end diff --git a/lib/rubygems/commands/unpack_command.rb b/lib/rubygems/commands/unpack_command.rb new file mode 100644 index 0000000000..ece24745a2 --- /dev/null +++ b/lib/rubygems/commands/unpack_command.rb @@ -0,0 +1,76 @@ +require 'fileutils' +require 'rubygems/command' +require 'rubygems/installer' +require 'rubygems/version_option' + +class Gem::Commands::UnpackCommand < Gem::Command + + include Gem::VersionOption + + def initialize + super 'unpack', 'Unpack an installed gem to the current directory', + :version => Gem::Requirement.default + add_version_option + end + + def arguments # :nodoc: + "GEMNAME name of gem to unpack" + end + + def defaults_str # :nodoc: + "--version '#{Gem::Requirement.default}'" + end + + def usage # :nodoc: + "#{program_name} GEMNAME" + end + + #-- + # TODO: allow, e.g., 'gem unpack rake-0.3.1'. Find a general solution for + # this, so that it works for uninstall as well. (And check other commands + # at the same time.) + def execute + gemname = get_one_gem_name + path = get_path(gemname, options[:version]) + if path + target_dir = File.basename(path).sub(/\.gem$/, '') + FileUtils.mkdir_p target_dir + Gem::Installer.new(path).unpack(File.expand_path(target_dir)) + say "Unpacked gem: '#{target_dir}'" + else + alert_error "Gem '#{gemname}' not installed." + end + end + + # Return the full path to the cached gem file matching the given + # name and version requirement. Returns 'nil' if no match. + # + # Example: + # + # get_path('rake', '> 0.4') # -> '/usr/lib/ruby/gems/1.8/cache/rake-0.4.2.gem' + # get_path('rake', '< 0.1') # -> nil + # get_path('rak') # -> nil (exact name required) + #-- + # TODO: This should be refactored so that it's a general service. I don't + # think any of our existing classes are the right place though. Just maybe + # 'Cache'? + # + # TODO: It just uses Gem.dir for now. What's an easy way to get the list of + # source directories? + def get_path(gemname, version_req) + return gemname if gemname =~ /\.gem$/i + specs = Gem::SourceIndex.from_installed_gems.search(/\A#{gemname}\z/, version_req) + selected = specs.sort_by { |s| s.version }.last + return nil if selected.nil? + # We expect to find (basename).gem in the 'cache' directory. + # Furthermore, the name match must be exact (ignoring case). + if gemname =~ /^#{selected.name}$/i + filename = selected.full_name + '.gem' + return File.join(Gem.dir, 'cache', filename) + else + return nil + end + end + +end + diff --git a/lib/rubygems/commands/update_command.rb b/lib/rubygems/commands/update_command.rb new file mode 100644 index 0000000000..e17ba2516a --- /dev/null +++ b/lib/rubygems/commands/update_command.rb @@ -0,0 +1,149 @@ +require 'rubygems/command' +require 'rubygems/install_update_options' +require 'rubygems/local_remote_options' +require 'rubygems/source_info_cache' +require 'rubygems/version_option' + +module Gem + module Commands + class UpdateCommand < Command + + include Gem::InstallUpdateOptions + include Gem::LocalRemoteOptions + include Gem::VersionOption + + def initialize + super( + 'update', + 'Update the named gems (or all installed gems) in the local repository', + { + :generate_rdoc => true, + :generate_ri => true, + :force => false, + :test => false, + :install_dir => Gem.dir + }) + + add_install_update_options + + add_option('--system', + 'Update the RubyGems system software') do |value, options| + options[:system] = value + end + + add_local_remote_options + + add_platform_option + end + + def arguments # :nodoc: + "GEMNAME name of gem to update" + end + + def defaults_str # :nodoc: + "--rdoc --ri --no-force --no-test\n" + + "--install-dir #{Gem.dir}" + end + + def usage # :nodoc: + "#{program_name} GEMNAME [GEMNAME ...]" + end + + def execute + if options[:system] then + say "Updating RubyGems..." + + unless options[:args].empty? then + fail "No gem names are allowed with the --system option" + end + + options[:args] = ["rubygems-update"] + else + say "Updating installed gems..." + end + + hig = highest_installed_gems = {} + + Gem::SourceIndex.from_installed_gems.each do |name, spec| + if hig[spec.name].nil? or hig[spec.name].version < spec.version + hig[spec.name] = spec + end + end + + remote_gemspecs = Gem::SourceInfoCache.search(//) + + gems_to_update = if options[:args].empty? then + which_to_update(highest_installed_gems, remote_gemspecs) + else + options[:args] + end + + options[:domain] = :remote # install from remote source + + # HACK use the real API + install_command = Gem::CommandManager.instance['install'] + + gems_to_update.uniq.sort.each do |name| + say "Attempting remote update of #{name}" + options[:args] = [name] + options[:ignore_dependencies] = true # HACK skip seen gems instead + install_command.merge_options(options) + install_command.execute + end + + if gems_to_update.include?("rubygems-update") then + latest_ruby_gem = remote_gemspecs.select { |s| + s.name == 'rubygems-update' + }.sort_by { |s| + s.version + }.last + + say "Updating version of RubyGems to #{latest_ruby_gem.version}" + installed = do_rubygems_update(latest_ruby_gem.version.to_s) + + say "RubyGems system software updated" if installed + else + say "Gems: [#{gems_to_update.uniq.sort.collect{|g| g.to_s}.join(', ')}] updated" + end + end + + def do_rubygems_update(version_string) + args = [] + args.push '--prefix', Gem.prefix unless Gem.prefix.nil? + args << '--no-rdoc' unless options[:generate_rdoc] + args << '--no-ri' unless options[:generate_ri] + + update_dir = File.join(Gem.dir, 'gems', + "rubygems-update-#{version_string}") + + success = false + + Dir.chdir update_dir do + say "Installing RubyGems #{version_string}" + setup_cmd = "#{Gem.ruby} setup.rb #{args.join ' '}" + + # Make sure old rubygems isn't loaded + if Gem.win_platform? then + system "set RUBYOPT= & #{setup_cmd}" + else + system "RUBYOPT=\"\" #{setup_cmd}" + end + end + end + + def which_to_update(highest_installed_gems, remote_gemspecs) + result = [] + highest_installed_gems.each do |l_name, l_spec| + highest_remote_gem = + remote_gemspecs.select { |spec| spec.name == l_name }. + sort_by { |spec| spec.version }. + last + if highest_remote_gem and l_spec.version < highest_remote_gem.version + result << l_name + end + end + result + end + end + end +end diff --git a/lib/rubygems/commands/which_command.rb b/lib/rubygems/commands/which_command.rb new file mode 100644 index 0000000000..b42244ce7d --- /dev/null +++ b/lib/rubygems/commands/which_command.rb @@ -0,0 +1,86 @@ +require 'rubygems/command' +require 'rubygems/gem_path_searcher' + +class Gem::Commands::WhichCommand < Gem::Command + + EXT = %w[.rb .rbw .so .dll] # HACK + + def initialize + super 'which', 'Find the location of a library', + :search_gems_first => false, :show_all => false + + add_option '-a', '--[no-]all', 'show all matching files' do |show_all, options| + options[:show_all] = show_all + end + + add_option '-g', '--[no-]gems-first', + 'search gems before non-gems' do |gems_first, options| + options[:search_gems_first] = gems_first + end + end + + def arguments # :nodoc: + "FILE name of file to find" + end + + def defaults_str # :nodoc: + "--no-gems-first --no-all" + end + + def usage # :nodoc: + "#{program_name} FILE [FILE ...]" + end + + def execute + searcher = Gem::GemPathSearcher.new + + options[:args].each do |arg| + dirs = $LOAD_PATH + spec = searcher.find arg + + if spec then + if options[:search_gems_first] then + dirs = gem_paths(spec) + $LOAD_PATH + else + dirs = $LOAD_PATH + gem_paths(spec) + end + + say "(checking gem #{spec.full_name} for #{arg})" if + Gem.configuration.verbose + end + + paths = find_paths arg, dirs + + if paths.empty? then + say "Can't find #{arg}" + else + say paths + end + end + end + + def find_paths(package_name, dirs) + result = [] + + dirs.each do |dir| + EXT.each do |ext| + full_path = File.join dir, "#{package_name}#{ext}" + if File.exist? full_path then + result << full_path + return result unless options[:show_all] + end + end + end + + result + end + + def gem_paths(spec) + spec.require_paths.collect { |d| File.join spec.full_gem_path, d } + end + + def usage # :nodoc: + "#{program_name} FILE [...]" + end + +end diff --git a/lib/rubygems/config_file.rb b/lib/rubygems/config_file.rb new file mode 100644 index 0000000000..5bca0bd14e --- /dev/null +++ b/lib/rubygems/config_file.rb @@ -0,0 +1,224 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'yaml' +require 'rubygems' + +# Store the gem command options specified in the configuration file. The +# config file object acts much like a hash. + +class Gem::ConfigFile + + DEFAULT_BACKTRACE = false + DEFAULT_BENCHMARK = false + DEFAULT_BULK_THRESHOLD = 1000 + DEFAULT_VERBOSITY = true + DEFAULT_UPDATE_SOURCES = true + + # List of arguments supplied to the config file object. + attr_reader :args + + # True if we print backtraces on errors. + attr_writer :backtrace + + # True if we are benchmarking this run. + attr_accessor :benchmark + + # Bulk threshold value. If the number of missing gems are above + # this threshold value, then a bulk download technique is used. + attr_accessor :bulk_threshold + + # Verbose level of output: + # * false -- No output + # * true -- Normal output + # * :loud -- Extra output + attr_accessor :verbose + + # True if we want to update the SourceInfoCache every time, false otherwise + attr_accessor :update_sources + + # Create the config file object. +args+ is the list of arguments + # from the command line. + # + # The following command line options are handled early here rather + # than later at the time most command options are processed. + # + # * --config-file and --config-file==NAME -- Obviously these need + # to be handled by the ConfigFile object to ensure we get the + # right config file. + # + # * --backtrace -- Backtrace needs to be turned on early so that + # errors before normal option parsing can be properly handled. + # + # * --debug -- Enable Ruby level debug messages. Handled early + # for the same reason as --backtrace. + # + def initialize(arg_list) + @config_file_name = nil + need_config_file_name = false + + arg_list = arg_list.map do |arg| + if need_config_file_name then + @config_file_name = arg + nil + elsif arg =~ /^--config-file=(.*)/ then + @config_file_name = $1 + nil + elsif arg =~ /^--config-file$/ then + need_config_file_name = true + nil + else + arg + end + end.compact + + @backtrace = DEFAULT_BACKTRACE + @benchmark = DEFAULT_BENCHMARK + @bulk_threshold = DEFAULT_BULK_THRESHOLD + @verbose = DEFAULT_VERBOSITY + @update_sources = DEFAULT_UPDATE_SOURCES + + begin + # HACK $SAFE ok? + @hash = open(config_file_name.dup.untaint) {|f| YAML.load(f) } + rescue ArgumentError + warn "Failed to load #{config_file_name}" + rescue Errno::ENOENT + # Ignore missing config file error. + rescue Errno::EACCES + warn "Failed to load #{config_file_name} due to permissions problem." + end + + @hash ||= {} + + # HACK these override command-line args, which is bad + @backtrace = @hash[:backtrace] if @hash.key? :backtrace + @benchmark = @hash[:benchmark] if @hash.key? :benchmark + @bulk_threshold = @hash[:bulk_threshold] if @hash.key? :bulk_threshold + Gem.sources.replace @hash[:sources] if @hash.key? :sources + @verbose = @hash[:verbose] if @hash.key? :verbose + @update_sources = @hash[:update_sources] if @hash.key? :update_sources + + handle_arguments arg_list + end + + # True if the backtrace option has been specified, or debug is on. + def backtrace + @backtrace or $DEBUG + end + + # The name of the configuration file. + def config_file_name + @config_file_name || Gem.config_file + end + + # Delegates to @hash + def each(&block) + hash = @hash.dup + hash.delete :update_sources + hash.delete :verbose + hash.delete :benchmark + hash.delete :backtrace + hash.delete :bulk_threshold + + yield :update_sources, @update_sources + yield :verbose, @verbose + yield :benchmark, @benchmark + yield :backtrace, @backtrace + yield :bulk_threshold, @bulk_threshold + + yield 'config_file_name', @config_file_name if @config_file_name + + hash.each(&block) + end + + # Handle the command arguments. + def handle_arguments(arg_list) + @args = [] + + arg_list.each do |arg| + case arg + when /^--(backtrace|traceback)$/ then + @backtrace = true + when /^--bench(mark)?$/ then + @benchmark = true + when /^--debug$/ then + $DEBUG = true + else + @args << arg + end + end + end + + # Really verbose mode gives you extra output. + def really_verbose + case verbose + when true, false, nil then false + else true + end + end + + # to_yaml only overwrites things you can't override on the command line. + def to_yaml # :nodoc: + yaml_hash = {} + yaml_hash[:backtrace] = @hash.key?(:backtrace) ? @hash[:backtrace] : + DEFAULT_BACKTRACE + yaml_hash[:benchmark] = @hash.key?(:benchmark) ? @hash[:benchmark] : + DEFAULT_BENCHMARK + yaml_hash[:bulk_threshold] = @hash.key?(:bulk_threshold) ? + @hash[:bulk_threshold] : DEFAULT_BULK_THRESHOLD + yaml_hash[:sources] = Gem.sources + yaml_hash[:update_sources] = @hash.key?(:update_sources) ? + @hash[:update_sources] : DEFAULT_UPDATE_SOURCES + yaml_hash[:verbose] = @hash.key?(:verbose) ? @hash[:verbose] : + DEFAULT_VERBOSITY + + keys = yaml_hash.keys.map { |key| key.to_s } + keys << 'debug' + re = Regexp.union(*keys) + + @hash.each do |key, value| + key = key.to_s + next if key =~ re + yaml_hash[key.to_s] = value + end + + yaml_hash.to_yaml + end + + # Writes out this config file, replacing its source. + def write + File.open config_file_name, 'w' do |fp| + fp.write self.to_yaml + end + end + + # Return the configuration information for +key+. + def [](key) + @hash[key.to_s] + end + + # Set configuration option +key+ to +value+. + def []=(key, value) + @hash[key.to_s] = value + end + + def ==(other) # :nodoc: + self.class === other and + @backtrace == other.backtrace and + @benchmark == other.benchmark and + @bulk_threshold == other.bulk_threshold and + @verbose == other.verbose and + @update_sources == other.update_sources and + @hash == other.hash + end + + protected + + attr_reader :hash + +end + diff --git a/lib/rubygems/custom_require.rb b/lib/rubygems/custom_require.rb new file mode 100755 index 0000000000..598ec3ef98 --- /dev/null +++ b/lib/rubygems/custom_require.rb @@ -0,0 +1,38 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems' + +module Kernel + alias gem_original_require require # :nodoc: + + # + # We replace Ruby's require with our own, which is capable of + # loading gems on demand. + # + # When you call require 'x', this is what happens: + # * If the file can be loaded from the existing Ruby loadpath, it + # is. + # * Otherwise, installed gems are searched for a file that matches. + # If it's found in gem 'y', that gem is activated (added to the + # loadpath). + # + # The normal require functionality of returning false if + # that file has already been loaded is preserved. + # + def require(path) # :nodoc: + gem_original_require path + rescue LoadError => load_error + if load_error.message =~ /\A[Nn]o such file to load -- #{Regexp.escape path}\z/ and + spec = Gem.searcher.find(path) then + Gem.activate(spec.name, false, "= #{spec.version}") + gem_original_require path + else + raise load_error + end + end +end # module Kernel + diff --git a/lib/rubygems/dependency.rb b/lib/rubygems/dependency.rb new file mode 100644 index 0000000000..be731d564e --- /dev/null +++ b/lib/rubygems/dependency.rb @@ -0,0 +1,65 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems' + +## +# The Dependency class holds a Gem name and a Gem::Requirement +class Gem::Dependency + + attr_accessor :name + + attr_writer :version_requirements + + def <=>(other) + [@name] <=> [other.name] + end + + ## + # Constructs the dependency + # + # name:: [String] name of the Gem + # version_requirements:: [String Array] version requirement (e.g. ["> 1.2"]) + # + def initialize(name, version_requirements) + @name = name + @version_requirements = Gem::Requirement.create version_requirements + @version_requirement = nil # Avoid warnings. + end + + def version_requirements + normalize if defined? @version_requirement and @version_requirement + @version_requirements + end + + def requirement_list + version_requirements.as_list + end + + alias requirements_list requirement_list + + def normalize + ver = @version_requirement.instance_eval { @version } + @version_requirements = Gem::Requirement.new([ver]) + @version_requirement = nil + end + + def to_s # :nodoc: + "#{name} (#{version_requirements})" + end + + def ==(other) # :nodoc: + self.class === other && + self.name == other.name && + self.version_requirements == other.version_requirements + end + + def hash + name.hash + version_requirements.hash + end + +end + diff --git a/lib/rubygems/dependency_installer.rb b/lib/rubygems/dependency_installer.rb new file mode 100644 index 0000000000..49afc76c79 --- /dev/null +++ b/lib/rubygems/dependency_installer.rb @@ -0,0 +1,219 @@ +require 'rubygems' +require 'rubygems/dependency_list' +require 'rubygems/installer' +require 'rubygems/source_info_cache' +require 'rubygems/user_interaction' + +class Gem::DependencyInstaller + + include Gem::UserInteraction + + attr_reader :gems_to_install + attr_reader :installed_gems + + DEFAULT_OPTIONS = { + :env_shebang => false, + :domain => :both, # HACK dup + :force => false, + :ignore_dependencies => false, + :security_policy => Gem::Security::NoSecurity, # HACK AlmostNo? Low? + :wrappers => true + } + + ## + # Creates a new installer instance that will install +gem_name+ using + # version requirement +version+ and +options+. + # + # Options are: + # :env_shebang:: See Gem::Installer::new. + # :domain:: :local, :remote, or :both. :local only searches gems in the + # current directory. :remote searches only gems in Gem::sources. + # :both searches both. + # :force:: See Gem::Installer#install. + # :ignore_dependencies: Don't install any dependencies. + # :install_dir: See Gem::Installer#install. + # :security_policy: See Gem::Installer::new and Gem::Security. + # :wrappers: See Gem::Installer::new + def initialize(gem_name, version = nil, options = {}) + options = DEFAULT_OPTIONS.merge options + @env_shebang = options[:env_shebang] + @domain = options[:domain] + @force = options[:force] + @ignore_dependencies = options[:ignore_dependencies] + @install_dir = options[:install_dir] || Gem.dir + @security_policy = options[:security_policy] + @wrappers = options[:wrappers] + + @installed_gems = [] + + spec_and_source = nil + + local_gems = Dir["#{gem_name}*"].sort.reverse + unless local_gems.empty? then + local_gems.each do |gem_file| + next unless gem_file =~ /gem$/ + begin + spec = Gem::Format.from_file_by_path(gem_file).spec + spec_and_source = [spec, gem_file] + break + rescue SystemCallError, Gem::Package::FormatError + end + end + end + + if spec_and_source.nil? then + version ||= Gem::Requirement.default + @dep = Gem::Dependency.new gem_name, version + spec_and_sources = find_gems_with_sources(@dep).reverse + + spec_and_source = spec_and_sources.find do |spec, source| + Gem::Platform.match spec.platform + end + end + + if spec_and_source.nil? then + raise Gem::GemNotFoundException, + "could not find #{gem_name} locally or in a repository" + end + + @specs_and_sources = [spec_and_source] + + gather_dependencies + 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 prefered over older gems, and + # local gems prefered over remote gems. + def find_gems_with_sources(dep) + gems_and_sources = [] + + if @domain == :both or @domain == :local then + Dir[File.join(Dir.pwd, "#{dep.name}-[0-9]*.gem")].each do |gem_file| + spec = Gem::Format.from_file_by_path(gem_file).spec + gems_and_sources << [spec, gem_file] if spec.name == dep.name + end + end + + if @domain == :both or @domain == :remote then + gems_and_sources.push(*Gem::SourceInfoCache.search_with_source(dep, true)) + end + + gems_and_sources.sort_by do |gem, source| + [gem, source !~ /^http:\/\// ? 1 : 0] # local gems win + end + end + + ## + # Moves the gem +spec+ from +source_uri+ to the cache dir unless it is + # already there. If the source_uri is local the gem cache dir copy is + # always replaced. + def download(spec, source_uri) + gem_file_name = "#{spec.full_name}.gem" + local_gem_path = File.join @install_dir, 'cache', gem_file_name + + Gem.ensure_gem_subdirectories @install_dir + + source_uri = URI.parse source_uri unless URI::Generic === source_uri + scheme = source_uri.scheme + + # URI.parse gets confused by MS Windows paths with forward slashes. + scheme = nil if scheme =~ /^[a-z]$/i + + case scheme + when 'http' then + unless File.exist? local_gem_path then + say "Downloading gem #{gem_file_name}" if + Gem.configuration.really_verbose + + remote_gem_path = source_uri + "gems/#{gem_file_name}" + + gem = Gem::RemoteFetcher.fetcher.fetch_path remote_gem_path + + File.open local_gem_path, 'wb' do |fp| + fp.write gem + end + end + when nil, 'file' then # TODO test for local overriding cache + begin + FileUtils.cp source_uri.to_s, local_gem_path + rescue Errno::EACCES + local_gem_path = source_uri.to_s + end + + say "Using local gem #{local_gem_path}" if + Gem.configuration.really_verbose + else + raise Gem::InstallError, "unsupported URI scheme #{source_uri.scheme}" + end + + local_gem_path + end + + ## + # Gathers all dependencies necessary for the installation from local and + # remote sources unless the ignore_dependencies was given. + def gather_dependencies + specs = @specs_and_sources.map { |spec,_| spec } + + dependency_list = Gem::DependencyList.new + dependency_list.add(*specs) + + unless @ignore_dependencies then + to_do = specs.dup + seen = {} + + until to_do.empty? do + spec = to_do.shift + next if spec.nil? or seen[spec.name] + seen[spec.name] = true + + spec.dependencies.each do |dep| + results = find_gems_with_sources(dep).reverse # local gems first + + results.each do |dep_spec, source_uri| + next if seen[dep_spec.name] + @specs_and_sources << [dep_spec, source_uri] + dependency_list.add dep_spec + to_do.push dep_spec + end + end + end + end + + @gems_to_install = dependency_list.dependency_order.reverse + end + + ## + # Installs the gem and all its dependencies. + def install + spec_dir = File.join @install_dir, 'specifications' + source_index = Gem::SourceIndex.from_gems_in spec_dir + + @gems_to_install.each do |spec| + last = spec == @gems_to_install.last + # HACK is this test for full_name acceptable? + next if source_index.any? { |n,_| n == spec.full_name } and not last + + say "Installing gem #{spec.full_name}" if Gem.configuration.really_verbose + + _, source_uri = @specs_and_sources.assoc spec + local_gem_path = download spec, source_uri + + inst = Gem::Installer.new local_gem_path, + :env_shebang => @env_shebang, + :force => @force, + :ignore_dependencies => @ignore_dependencies, + :install_dir => @install_dir, + :security_policy => @security_policy, + :wrappers => @wrappers + + spec = inst.install + + @installed_gems << spec + end + end + +end + diff --git a/lib/rubygems/dependency_list.rb b/lib/rubygems/dependency_list.rb new file mode 100644 index 0000000000..81aa65bfb2 --- /dev/null +++ b/lib/rubygems/dependency_list.rb @@ -0,0 +1,165 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'tsort' + +class Gem::DependencyList + + include TSort + + def self.from_source_index(src_index) + deps = new + + src_index.each do |full_name, spec| + deps.add spec + end + + deps + end + + def initialize + @specs = [] + end + + # Adds +gemspecs+ to the dependency list. + def add(*gemspecs) + @specs.push(*gemspecs) + end + + # Return a list of the specifications in the dependency list, + # sorted in order so that no spec in the list depends on a gem + # earlier in the list. + # + # This is useful when removing gems from a set of installed gems. + # By removing them in the returned order, you don't get into as + # many dependency issues. + # + # If there are circular dependencies (yuck!), then gems will be + # returned in order until only the circular dependents and anything + # they reference are left. Then arbitrary gemspecs will be returned + # until the circular dependency is broken, after which gems will be + # returned in dependency order again. + def dependency_order + sorted = strongly_connected_components.flatten + + result = [] + seen = {} + + sorted.each do |spec| + if index = seen[spec.name] then + if result[index].version < spec.version then + result[index] = spec + end + else + seen[spec.name] = result.length + result << spec + end + end + + result.reverse + end + + def find_name(full_name) + @specs.find { |spec| spec.full_name == full_name } + end + + # Are all the dependencies in the list satisfied? + def ok? + @specs.all? do |spec| + spec.dependencies.all? do |dep| + @specs.find { |s| s.satisfies_requirement? dep } + end + end + end + + # Is is ok to remove a gem from the dependency list? + # + # If removing the gemspec creates breaks a currently ok dependency, + # then it is NOT ok to remove the gem. + def ok_to_remove?(full_name) + gem_to_remove = find_name full_name + + siblings = @specs.find_all { |s| + s.name == gem_to_remove.name && + s.full_name != gem_to_remove.full_name + } + + deps = [] + + @specs.each do |spec| + spec.dependencies.each do |dep| + deps << dep if gem_to_remove.satisfies_requirement?(dep) + end + end + + deps.all? { |dep| + siblings.any? { |s| + s.satisfies_requirement? dep + } + } + end + + def remove_by_name(full_name) + @specs.delete_if { |spec| spec.full_name == full_name } + end + + # Return a hash of predecessors. result[spec] is an + # Array of gemspecs that have a dependency satisfied by the named + # spec. + def spec_predecessors + result = Hash.new { |h,k| h[k] = [] } + + specs = @specs.sort.reverse + + specs.each do |spec| + specs.each do |other| + next if spec == other + + other.dependencies.each do |dep| + if spec.satisfies_requirement? dep then + result[spec] << other + end + end + end + end + + result + end + + def tsort_each_node(&block) + @specs.each(&block) + end + + def tsort_each_child(node, &block) + specs = @specs.sort.reverse + + node.dependencies.each do |dep| + specs.each do |spec| + if spec.satisfies_requirement? dep then + begin + yield spec + rescue TSort::Cyclic + end + break + end + end + end + end + + private + + # Count the number of gemspecs in the list +specs+ that are not in + # +ignored+. + def active_count(specs, ignored) + result = 0 + specs.each do |spec| + result += 1 unless ignored[spec.full_name] + end + result + end + +end + diff --git a/lib/rubygems/digest/digest_adapter.rb b/lib/rubygems/digest/digest_adapter.rb new file mode 100755 index 0000000000..d5a00b059d --- /dev/null +++ b/lib/rubygems/digest/digest_adapter.rb @@ -0,0 +1,40 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +module Gem + + # There is an incompatibility between the way Ruby 1.8.5 and 1.8.6 + # handles digests. This DigestAdapter will take a pre-1.8.6 digest + # and adapt it to the 1.8.6 API. + # + # Note that only the digest and hexdigest methods are adapted, + # since these are the only functions used by Gems. + # + class DigestAdapter + + # Initialize a digest adapter. + def initialize(digest_class) + @digest_class = digest_class + end + + # Return a new digester. Since we are only implementing the stateless + # methods, we will return ourself as the instance. + def new + self + end + + # Return the digest of +string+ as a hex string. + def hexdigest(string) + @digest_class.new(string).hexdigest + end + + # Return the digest of +string+ as a binary string. + def digest(string) + @digest_class.new(string).digest + end + end +end \ No newline at end of file diff --git a/lib/rubygems/digest/md5.rb b/lib/rubygems/digest/md5.rb new file mode 100755 index 0000000000..f924579c08 --- /dev/null +++ b/lib/rubygems/digest/md5.rb @@ -0,0 +1,23 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'digest/md5' + +# :stopdoc: +module Gem + if RUBY_VERSION >= '1.8.6' + MD5 = Digest::MD5 + else + require 'rubygems/digest/digest_adapter' + MD5 = DigestAdapter.new(Digest::MD5) + def MD5.md5(string) + self.hexdigest(string) + end + end +end +# :startdoc: + diff --git a/lib/rubygems/digest/sha1.rb b/lib/rubygems/digest/sha1.rb new file mode 100755 index 0000000000..2a6245dcd9 --- /dev/null +++ b/lib/rubygems/digest/sha1.rb @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'digest/sha1' + +module Gem + if RUBY_VERSION >= '1.8.6' + SHA1 = Digest::SHA1 + else + require 'rubygems/digest/digest_adapter' + SHA1 = DigestAdapter.new(Digest::SHA1) + end +end \ No newline at end of file diff --git a/lib/rubygems/digest/sha2.rb b/lib/rubygems/digest/sha2.rb new file mode 100755 index 0000000000..7bef16aed2 --- /dev/null +++ b/lib/rubygems/digest/sha2.rb @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'digest/sha2' + +module Gem + if RUBY_VERSION >= '1.8.6' + SHA256 = Digest::SHA256 + else + require 'rubygems/digest/digest_adapter' + SHA256 = DigestAdapter.new(Digest::SHA256) + end +end diff --git a/lib/rubygems/doc_manager.rb b/lib/rubygems/doc_manager.rb new file mode 100644 index 0000000000..8d9b4a7b23 --- /dev/null +++ b/lib/rubygems/doc_manager.rb @@ -0,0 +1,161 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'fileutils' + +module Gem + + class DocManager + + include UserInteraction + + # Create a document manager for the given gem spec. + # + # spec:: The Gem::Specification object representing the gem. + # rdoc_args:: Optional arguments for RDoc (template etc.) as a String. + # + def initialize(spec, rdoc_args="") + @spec = spec + @doc_dir = File.join(spec.installation_path, "doc", spec.full_name) + @rdoc_args = rdoc_args.nil? ? [] : rdoc_args.split + end + + # Is the RDoc documentation installed? + def rdoc_installed? + return File.exist?(File.join(@doc_dir, "rdoc")) + end + + # Generate the RI documents for this gem spec. + # + # Note that if both RI and RDoc documents are generated from the + # same process, the RI docs should be done first (a likely bug in + # RDoc will cause RI docs generation to fail if run after RDoc). + def generate_ri + if @spec.has_rdoc then + load_rdoc + install_ri # RDoc bug, ri goes first + end + + FileUtils.mkdir_p @doc_dir unless File.exist?(@doc_dir) + end + + # Generate the RDoc documents for this gem spec. + # + # Note that if both RI and RDoc documents are generated from the + # same process, the RI docs should be done first (a likely bug in + # RDoc will cause RI docs generation to fail if run after RDoc). + def generate_rdoc + if @spec.has_rdoc then + load_rdoc + install_rdoc + end + + FileUtils.mkdir_p @doc_dir unless File.exist?(@doc_dir) + end + + # Load the RDoc documentation generator library. + def load_rdoc + if File.exist?(@doc_dir) && !File.writable?(@doc_dir) then + raise Gem::FilePermissionError.new(@doc_dir) + end + + FileUtils.mkdir_p @doc_dir unless File.exist?(@doc_dir) + + begin + require 'rdoc/rdoc' + rescue LoadError => e + raise Gem::DocumentError, + "ERROR: RDoc documentation generator not installed!" + end + end + + def install_rdoc + rdoc_dir = File.join @doc_dir, 'rdoc' + + FileUtils.rm_rf rdoc_dir + + say "Installing RDoc documentation for #{@spec.full_name}..." + run_rdoc '--op', rdoc_dir + end + + def install_ri + ri_dir = File.join @doc_dir, 'ri' + + FileUtils.rm_rf ri_dir + + say "Installing ri documentation for #{@spec.full_name}..." + run_rdoc '--ri', '--op', ri_dir + end + + def run_rdoc(*args) + args << @spec.rdoc_options + args << DocManager.configured_args + args << '--quiet' + args << @spec.require_paths.clone + args << @spec.extra_rdoc_files + args.flatten! + + r = RDoc::RDoc.new + + old_pwd = Dir.pwd + Dir.chdir(@spec.full_gem_path) + begin + r.document args + rescue Errno::EACCES => e + dirname = File.dirname e.message.split("-")[1].strip + raise Gem::FilePermissionError.new(dirname) + rescue RuntimeError => ex + alert_error "While generating documentation for #{@spec.full_name}" + ui.errs.puts "... MESSAGE: #{ex}" + ui.errs.puts "... RDOC args: #{args.join(' ')}" + ui.errs.puts "\t#{ex.backtrace.join "\n\t"}" if + Gem.configuration.backtrace + ui.errs.puts "(continuing with the rest of the installation)" + ensure + Dir.chdir(old_pwd) + end + end + + def uninstall_doc + raise Gem::FilePermissionError.new(@spec.installation_path) unless + File.writable? @spec.installation_path + + original_name = [ + @spec.name, @spec.version, @spec.original_platform].join '-' + + doc_dir = File.join @spec.installation_path, 'doc', @spec.full_name + unless File.directory? doc_dir then + doc_dir = File.join @spec.installation_path, 'doc', original_name + end + + FileUtils.rm_rf doc_dir + + ri_dir = File.join @spec.installation_path, 'ri', @spec.full_name + + unless File.directory? ri_dir then + ri_dir = File.join @spec.installation_path, 'ri', original_name + end + + FileUtils.rm_rf ri_dir + end + + class << self + def configured_args + @configured_args ||= [] + end + + def configured_args=(args) + case args + when Array + @configured_args = args + when String + @configured_args = args.split + end + end + end + + end +end diff --git a/lib/rubygems/exceptions.rb b/lib/rubygems/exceptions.rb new file mode 100644 index 0000000000..294dad5748 --- /dev/null +++ b/lib/rubygems/exceptions.rb @@ -0,0 +1,63 @@ +require 'rubygems' + +## +# Base exception class for RubyGems. All exception raised by RubyGems are a +# subclass of this one. +class Gem::Exception < RuntimeError; end + +class Gem::CommandLineError < Gem::Exception; end + +class Gem::DependencyError < Gem::Exception; end + +class Gem::DependencyRemovalException < Gem::Exception; end + +class Gem::DocumentError < Gem::Exception; end + +## +# Potentially raised when a specification is validated. +class Gem::EndOfYAMLException < Gem::Exception; end + +## +# Signals that a file permission error is preventing the user from +# installing in the requested directories. +class Gem::FilePermissionError < Gem::Exception + def initialize(path) + super("You don't have write permissions into the #{path} directory.") + end +end + +## +# Used to raise parsing and loading errors +class Gem::FormatException < Gem::Exception + attr_accessor :file_path +end + +class Gem::GemNotFoundException < Gem::Exception; end + +class Gem::InstallError < Gem::Exception; end + +## +# Potentially raised when a specification is validated. +class Gem::InvalidSpecificationException < Gem::Exception; end + +class Gem::OperationNotSupportedError < Gem::Exception; end + +## +# Signals that a remote operation cannot be conducted, probably due to not +# being connected (or just not finding host). +#-- +# TODO: create a method that tests connection to the preferred gems server. +# All code dealing with remote operations will want this. Failure in that +# method should raise this error. +class Gem::RemoteError < Gem::Exception; end + +class Gem::RemoteInstallationCancelled < Gem::Exception; end + +class Gem::RemoteInstallationSkipped < Gem::Exception; end + +## +# Represents an error communicating via HTTP. +class Gem::RemoteSourceException < Gem::Exception; end + +class Gem::VerificationError < Gem::Exception; end + diff --git a/lib/rubygems/ext.rb b/lib/rubygems/ext.rb new file mode 100644 index 0000000000..97ee762a4a --- /dev/null +++ b/lib/rubygems/ext.rb @@ -0,0 +1,18 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems' + +## +# Classes for building C extensions live here. + +module Gem::Ext; end + +require 'rubygems/ext/builder' +require 'rubygems/ext/configure_builder' +require 'rubygems/ext/ext_conf_builder' +require 'rubygems/ext/rake_builder' + diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb new file mode 100644 index 0000000000..576951a566 --- /dev/null +++ b/lib/rubygems/ext/builder.rb @@ -0,0 +1,56 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems/ext' + +class Gem::Ext::Builder + + def self.class_name + name =~ /Ext::(.*)Builder/ + $1.downcase + end + + def self.make(dest_path, results) + unless File.exist? 'Makefile' then + raise Gem::InstallError, "Makefile not found:\n\n#{results.join "\n"}" + end + + mf = File.read('Makefile') + mf = mf.gsub(/^RUBYARCHDIR\s*=\s*\$[^$]*/, "RUBYARCHDIR = #{dest_path}") + mf = mf.gsub(/^RUBYLIBDIR\s*=\s*\$[^$]*/, "RUBYLIBDIR = #{dest_path}") + + File.open('Makefile', 'wb') {|f| f.print mf} + + make_program = ENV['make'] + unless make_program then + make_program = (/mswin/ =~ RUBY_PLATFORM) ? 'nmake' : 'make' + end + + ['', ' install'].each do |target| + cmd = "#{make_program}#{target}" + results << cmd + results << `#{cmd} #{redirector}` + + raise Gem::InstallError, "make#{target} failed:\n\n#{results}" unless + $?.exitstatus.zero? + end + end + + def self.redirector + '2>&1' + end + + def self.run(command, results) + results << command + results << `#{command} #{redirector}` + + unless $?.exitstatus.zero? then + raise Gem::InstallError, "#{class_name} failed:\n\n#{results.join "\n"}" + end + end + +end + diff --git a/lib/rubygems/ext/configure_builder.rb b/lib/rubygems/ext/configure_builder.rb new file mode 100644 index 0000000000..1cde6915a7 --- /dev/null +++ b/lib/rubygems/ext/configure_builder.rb @@ -0,0 +1,24 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems/ext/builder' + +class Gem::Ext::ConfigureBuilder < Gem::Ext::Builder + + def self.build(extension, directory, dest_path, results) + unless File.exist?('Makefile') then + cmd = "sh ./configure --prefix=#{dest_path}" + + run cmd, results + end + + make dest_path, results + + results + end + +end + diff --git a/lib/rubygems/ext/ext_conf_builder.rb b/lib/rubygems/ext/ext_conf_builder.rb new file mode 100644 index 0000000000..cbe0e80821 --- /dev/null +++ b/lib/rubygems/ext/ext_conf_builder.rb @@ -0,0 +1,23 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems/ext/builder' + +class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder + + def self.build(extension, directory, dest_path, results) + cmd = "#{Gem.ruby} #{File.basename extension}" + cmd << " #{ARGV.join ' '}" unless ARGV.empty? + + run cmd, results + + make dest_path, results + + results + end + +end + diff --git a/lib/rubygems/ext/rake_builder.rb b/lib/rubygems/ext/rake_builder.rb new file mode 100644 index 0000000000..3772f6a00f --- /dev/null +++ b/lib/rubygems/ext/rake_builder.rb @@ -0,0 +1,27 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems/ext/builder' + +class Gem::Ext::RakeBuilder < Gem::Ext::Builder + + def self.build(extension, directory, dest_path, results) + if File.basename(extension) =~ /mkrf_conf/i then + cmd = "#{Gem.ruby} #{File.basename extension}" + cmd << " #{ARGV.join " "}" unless ARGV.empty? + run cmd, results + end + + cmd = ENV['rake'] || 'rake' + cmd << " RUBYARCHDIR=#{dest_path} RUBYLIBDIR=#{dest_path}" + + run cmd, results + + results + end + +end + diff --git a/lib/rubygems/format.rb b/lib/rubygems/format.rb new file mode 100644 index 0000000000..378a93018c --- /dev/null +++ b/lib/rubygems/format.rb @@ -0,0 +1,81 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'fileutils' + +require 'rubygems/package' + +module Gem + + ## + # The format class knows the guts of the RubyGem .gem file format + # and provides the capability to read gem files + # + class Format + attr_accessor :spec, :file_entries, :gem_path + extend Gem::UserInteraction + + ## + # Constructs an instance of a Format object, representing the gem's + # data structure. + # + # gem:: [String] The file name of the gem + # + def initialize(gem_path) + @gem_path = gem_path + end + + ## + # Reads the named gem file and returns a Format object, representing + # the data from the gem file + # + # file_path:: [String] Path to the gem file + # + def self.from_file_by_path(file_path, security_policy = nil) + format = nil + + unless File.exist?(file_path) + raise Gem::Exception, "Cannot load gem at [#{file_path}] in #{Dir.pwd}" + end + + # check for old version gem + if File.read(file_path, 20).include?("MD5SUM =") + #alert_warning "Gem #{file_path} is in old format." + require 'rubygems/old_format' + format = OldFormat.from_file_by_path(file_path) + else + begin + f = File.open(file_path, 'rb') + format = from_io(f, file_path, security_policy) + ensure + f.close unless f.closed? + end + end + + return format + end + + ## + # Reads a gem from an io stream and returns a Format object, representing + # the data from the gem file + # + # io:: [IO] Stream from which to read the gem + # + def self.from_io(io, gem_path="(io)", security_policy = nil) + format = self.new(gem_path) + Package.open_from_io(io, 'r', security_policy) do |pkg| + format.spec = pkg.metadata + format.file_entries = [] + pkg.each do |entry| + format.file_entries << [{"size" => entry.size, "mode" => entry.mode, + "path" => entry.full_name}, entry.read] + end + end + format + end + + end +end diff --git a/lib/rubygems/gem_open_uri.rb b/lib/rubygems/gem_open_uri.rb new file mode 100644 index 0000000000..6e35413b37 --- /dev/null +++ b/lib/rubygems/gem_open_uri.rb @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +if RUBY_VERSION < "1.9" + require 'rubygems/open-uri' +else + require 'open-uri' +end diff --git a/lib/rubygems/gem_openssl.rb b/lib/rubygems/gem_openssl.rb new file mode 100644 index 0000000000..17e7d0f2bf --- /dev/null +++ b/lib/rubygems/gem_openssl.rb @@ -0,0 +1,83 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +# Some system might not have OpenSSL installed, therefore the core +# library file openssl might not be available. We localize testing +# for the presence of OpenSSL in this file. + +module Gem + class << self + # Is SSL (used by the signing commands) available on this + # platform? + def ssl_available? + require 'rubygems/gem_openssl' + @ssl_available + end + + # Set the value of the ssl_avilable flag. + attr_writer :ssl_available + + # Ensure that SSL is available. Throw an exception if it is not. + def ensure_ssl_available + unless ssl_available? + fail Gem::Exception, "SSL is not installed on this system" + end + end + end +end + +begin + require 'openssl' + + # Reference a constant defined in the .rb portion of ssl (just to + # make sure that part is loaded too). + + dummy = OpenSSL::Digest::SHA1 + + Gem.ssl_available = true + + class OpenSSL::X509::Certificate # :nodoc: + # Check the validity of this certificate. + def check_validity(issuer_cert = nil, time = Time.now) + ret = if @not_before && @not_before > time + [false, :expired, "not valid before '#@not_before'"] + elsif @not_after && @not_after < time + [false, :expired, "not valid after '#@not_after'"] + elsif issuer_cert && !verify(issuer_cert.public_key) + [false, :issuer, "#{issuer_cert.subject} is not issuer"] + else + [true, :ok, 'Valid certificate'] + end + + # return hash + { :is_valid => ret[0], :error => ret[1], :desc => ret[2] } + end + end + +rescue LoadError, StandardError + Gem.ssl_available = false +end + +module Gem::SSL + + # We make our own versions of the constants here. This allows us + # to reference the constants, even though some systems might not + # have SSL installed in the Ruby core package. + # + # These constants are only used during load time. At runtime, any + # method that makes a direct reference to SSL software must be + # protected with a Gem.ensure_ssl_available call. + # + if Gem.ssl_available? then + PKEY_RSA = OpenSSL::PKey::RSA + DIGEST_SHA1 = OpenSSL::Digest::SHA1 + else + PKEY_RSA = :rsa + DIGEST_SHA1 = :sha1 + end + +end + diff --git a/lib/rubygems/gem_path_searcher.rb b/lib/rubygems/gem_path_searcher.rb new file mode 100644 index 0000000000..dadad66289 --- /dev/null +++ b/lib/rubygems/gem_path_searcher.rb @@ -0,0 +1,84 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems' + +# +# GemPathSearcher has the capability to find loadable files inside +# gems. It generates data up front to speed up searches later. +# +class Gem::GemPathSearcher + + # + # Initialise the data we need to make searches later. + # + def initialize + # We want a record of all the installed gemspecs, in the order + # we wish to examine them. + @gemspecs = init_gemspecs + # Map gem spec to glob of full require_path directories. + # Preparing this information may speed up searches later. + @lib_dirs = {} + @gemspecs.each do |spec| + @lib_dirs[spec.object_id] = lib_dirs_for(spec) + end + end + + # + # Look in all the installed gems until a matching _path_ is found. + # Return the _gemspec_ of the gem where it was found. If no match + # is found, return nil. + # + # The gems are searched in alphabetical order, and in reverse + # version order. + # + # For example: + # + # find('log4r') # -> (log4r-1.1 spec) + # find('log4r.rb') # -> (log4r-1.1 spec) + # find('rake/rdoctask') # -> (rake-0.4.12 spec) + # find('foobarbaz') # -> nil + # + # Matching paths can have various suffixes ('.rb', '.so', and + # others), which may or may not already be attached to _file_. + # This method doesn't care about the full filename that matches; + # only that there is a match. + # + def find(path) + @gemspecs.each do |spec| + return spec if matching_file(spec, path) + end + nil + end + + private + + # Attempts to find a matching path using the require_paths of the + # given _spec_. + # + # Some of the intermediate results are cached in @lib_dirs for + # speed. + def matching_file(spec, path) # :doc: + glob = File.join @lib_dirs[spec.object_id], "#{path}#{Gem.suffix_pattern}" + return true unless Dir[glob].select { |f| File.file?(f.untaint) }.empty? + end + + # Return a list of all installed gemspecs, sorted by alphabetical + # order and in reverse version order. + def init_gemspecs + Gem.source_index.map { |_, spec| spec }.sort { |a,b| + (a.name <=> b.name).nonzero? || (b.version <=> a.version) + } + end + + # Returns library directories glob for a gemspec. For example, + # '/usr/local/lib/ruby/gems/1.8/gems/foobar-1.0/{lib,ext}' + def lib_dirs_for(spec) + "#{spec.full_gem_path}/{#{spec.require_paths.join(',')}}" + end + +end + diff --git a/lib/rubygems/gem_runner.rb b/lib/rubygems/gem_runner.rb new file mode 100644 index 0000000000..5f91398b5b --- /dev/null +++ b/lib/rubygems/gem_runner.rb @@ -0,0 +1,58 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems/command_manager' +require 'rubygems/config_file' +require 'rubygems/doc_manager' + +module Gem + + #################################################################### + # Run an instance of the gem program. + # + class GemRunner + + def initialize(options={}) + @command_manager_class = options[:command_manager] || Gem::CommandManager + @config_file_class = options[:config_file] || Gem::ConfigFile + @doc_manager_class = options[:doc_manager] || Gem::DocManager + end + + # Run the gem command with the following arguments. + def run(args) + start_time = Time.now + do_configuration(args) + cmd = @command_manager_class.instance + cmd.command_names.each do |command_name| + config_args = Gem.configuration[command_name] + config_args = case config_args + when String + config_args.split ' ' + else + Array(config_args) + end + Command.add_specific_extra_args command_name, config_args + end + cmd.run(Gem.configuration.args) + end_time = Time.now + if Gem.configuration.benchmark + printf "\nExecution time: %0.2f seconds.\n", end_time-start_time + puts "Press Enter to finish" + STDIN.gets + end + end + + private + + def do_configuration(args) + Gem.configuration = @config_file_class.new(args) + Gem.use_paths(Gem.configuration[:gemhome], Gem.configuration[:gempath]) + Gem::Command.extra_args = Gem.configuration[:gem] + @doc_manager_class.configured_args = Gem.configuration[:rdoc] + end + + end # class +end # module diff --git a/lib/rubygems/indexer.rb b/lib/rubygems/indexer.rb new file mode 100644 index 0000000000..8cb7735c29 --- /dev/null +++ b/lib/rubygems/indexer.rb @@ -0,0 +1,171 @@ +require 'fileutils' +require 'tmpdir' + +require 'rubygems' +require 'rubygems/format' + +begin + require 'builder/xchar' +rescue LoadError +end + +## +# Top level class for building the gem repository index. +class Gem::Indexer + + include Gem::UserInteraction + + ## + # Index install location + + attr_reader :dest_directory + + ## + # Index build directory + + attr_reader :directory + + # Create an indexer that will index the gems in +directory+. + def initialize(directory) + unless ''.respond_to? :to_xs then + fail "Gem::Indexer requires that the XML Builder library be installed:" \ + "\n\tgem install builder" + end + + @dest_directory = directory + @directory = File.join Dir.tmpdir, "gem_generate_index_#{$$}" + + marshal_name = "Marshal.#{Gem.marshal_version}" + + @master_index = Gem::Indexer::MasterIndexBuilder.new "yaml", @directory + @marshal_index = Gem::Indexer::MarshalIndexBuilder.new marshal_name, @directory + @quick_index = Gem::Indexer::QuickIndexBuilder.new "index", @directory + end + + # Build the index. + def build_index + @master_index.build do + @quick_index.build do + @marshal_index.build do + progress = ui.progress_reporter gem_file_list.size, + "Generating index for #{gem_file_list.size} gems in #{@dest_directory}", + "Loaded all gems" + + gem_file_list.each do |gemfile| + if File.size(gemfile.to_s) == 0 then + alert_warning "Skipping zero-length gem: #{gemfile}" + next + end + + begin + spec = Gem::Format.from_file_by_path(gemfile).spec + + original_name = if spec.platform == Gem::Platform::RUBY or + spec.platform.nil? then + spec.full_name + else + "#{spec.name}-#{spec.version}-#{spec.original_platform}" + end + + unless gemfile =~ /\/#{Regexp.escape spec.full_name}.*\.gem\z/i or + gemfile =~ /\/#{Regexp.escape original_name}.*\.gem\z/i then + alert_warning "Skipping misnamed gem: #{gemfile} => #{spec.full_name} (#{original_name})" + next + end + + abbreviate spec + sanitize spec + + @master_index.add spec + @quick_index.add spec + @marshal_index.add spec + + progress.updated spec.full_name + + rescue SignalException => e + alert_error "Recieved signal, exiting" + raise + rescue Exception => e + alert_error "Unable to process #{gemfile}\n#{e.message} (#{e.class})\n\t#{e.backtrace.join "\n\t"}" + end + end + + progress.done + + say "Generating master indexes (this may take a while)" + end + end + end + end + + def install_index + verbose = Gem.configuration.really_verbose + + say "Moving index into production dir #{@dest_directory}" if verbose + + files = @master_index.files + @quick_index.files + @marshal_index.files + + files.each do |file| + relative_name = file[/\A#{@directory}.(.*)/, 1] + dest_name = File.join @dest_directory, relative_name + + FileUtils.rm_rf dest_name, :verbose => verbose + FileUtils.mv file, @dest_directory, :verbose => verbose + end + end + + def generate_index + FileUtils.rm_rf @directory + FileUtils.mkdir_p @directory, :mode => 0700 + + build_index + install_index + rescue SignalException + ensure + FileUtils.rm_rf @directory + end + + # List of gem file names to index. + def gem_file_list + Dir.glob(File.join(@dest_directory, "gems", "*.gem")) + end + + # Abbreviate the spec for downloading. Abbreviated specs are only + # used for searching, downloading and related activities and do not + # need deployment specific information (e.g. list of files). So we + # abbreviate the spec, making it much smaller for quicker downloads. + def abbreviate(spec) + spec.files = [] + spec.test_files = [] + spec.rdoc_options = [] + spec.extra_rdoc_files = [] + spec.cert_chain = [] + spec + end + + # Sanitize the descriptive fields in the spec. Sometimes non-ASCII + # characters will garble the site index. Non-ASCII characters will + # be replaced by their XML entity equivalent. + def sanitize(spec) + spec.summary = sanitize_string(spec.summary) + spec.description = sanitize_string(spec.description) + spec.post_install_message = sanitize_string(spec.post_install_message) + spec.authors = spec.authors.collect { |a| sanitize_string(a) } + spec + end + + # Sanitize a single string. + def sanitize_string(string) + # HACK the #to_s is in here because RSpec has an Array of Arrays of + # Strings for authors. Need a way to disallow bad values on gempsec + # generation. (Probably won't happen.) + string ? string.to_s.to_xs : string + end + +end + +require 'rubygems/indexer/abstract_index_builder' +require 'rubygems/indexer/master_index_builder' +require 'rubygems/indexer/quick_index_builder' +require 'rubygems/indexer/marshal_index_builder' + diff --git a/lib/rubygems/indexer/abstract_index_builder.rb b/lib/rubygems/indexer/abstract_index_builder.rb new file mode 100644 index 0000000000..f25f21707b --- /dev/null +++ b/lib/rubygems/indexer/abstract_index_builder.rb @@ -0,0 +1,80 @@ +require 'zlib' + +require 'rubygems/indexer' + +# Abstract base class for building gem indicies. Uses the template pattern +# with subclass specialization in the +begin_index+, +end_index+ and +cleanup+ +# methods. +class Gem::Indexer::AbstractIndexBuilder + + # Directory to put index files in + attr_reader :directory + + # File name of the generated index + attr_reader :filename + + # List of written files/directories to move into production + attr_reader :files + + def initialize(filename, directory) + @filename = filename + @directory = directory + @files = [] + end + + # Build a Gem index. Yields to block to handle the details of the + # actual building. Calls +begin_index+, +end_index+ and +cleanup+ at + # appropriate times to customize basic operations. + def build + FileUtils.mkdir_p @directory unless File.exist? @directory + raise "not a directory: #{@directory}" unless File.directory? @directory + + file_path = File.join @directory, @filename + + @files << file_path + + File.open file_path, "wb" do |file| + @file = file + start_index + yield + end_index + end + cleanup + ensure + @file = nil + end + + # Compress the given file. + def compress(filename, ext="rz") + zipped = zip(File.open(filename, 'rb'){ |fp| fp.read }) + File.open "#{filename}.#{ext}", "wb" do |file| + file.write zipped + end + end + + # Called immediately before the yield in build. The index file is open and + # available as @file. + def start_index + end + + # Called immediately after the yield in build. The index file is still open + # and available as @file. + def end_index + end + + # Called from within builder after the index file has been closed. + def cleanup + end + + # Return an uncompressed version of a compressed string. + def unzip(string) + Zlib::Inflate.inflate(string) + end + + # Return a compressed version of the given string. + def zip(string) + Zlib::Deflate.deflate(string) + end + +end + diff --git a/lib/rubygems/indexer/marshal_index_builder.rb b/lib/rubygems/indexer/marshal_index_builder.rb new file mode 100644 index 0000000000..5e3ba7f5b9 --- /dev/null +++ b/lib/rubygems/indexer/marshal_index_builder.rb @@ -0,0 +1,8 @@ +require 'rubygems/indexer' + +# Construct the master Gem index file. +class Gem::Indexer::MarshalIndexBuilder < Gem::Indexer::MasterIndexBuilder + def end_index + @file.write @index.dump + end +end diff --git a/lib/rubygems/indexer/master_index_builder.rb b/lib/rubygems/indexer/master_index_builder.rb new file mode 100644 index 0000000000..f435c44e41 --- /dev/null +++ b/lib/rubygems/indexer/master_index_builder.rb @@ -0,0 +1,44 @@ +require 'rubygems/indexer' + +# Construct the master Gem index file. +class Gem::Indexer::MasterIndexBuilder < Gem::Indexer::AbstractIndexBuilder + + def start_index + super + @index = Gem::SourceIndex.new + end + + def end_index + super + @file.puts @index.to_yaml + end + + def cleanup + super + + index_file_name = File.join @directory, @filename + + compress index_file_name, "Z" + compressed_file_name = "#{index_file_name}.Z" + + paranoid index_file_name, compressed_file_name + + @files << compressed_file_name + end + + def add(spec) + @index.add_spec(spec) + end + + private + + def paranoid(fn, compressed_fn) + data = File.open(fn, 'rb') do |fp| fp.read end + compressed_data = File.open(compressed_fn, 'rb') do |fp| fp.read end + + if data != unzip(compressed_data) then + fail "Compressed file #{compressed_fn} does not match uncompressed file #{fn}" + end + end + +end diff --git a/lib/rubygems/indexer/quick_index_builder.rb b/lib/rubygems/indexer/quick_index_builder.rb new file mode 100644 index 0000000000..8805f3fe38 --- /dev/null +++ b/lib/rubygems/indexer/quick_index_builder.rb @@ -0,0 +1,48 @@ +require 'rubygems/indexer' + +# Construct a quick index file and all of the individual specs to support +# incremental loading. +class Gem::Indexer::QuickIndexBuilder < Gem::Indexer::AbstractIndexBuilder + + def initialize(filename, directory) + directory = File.join directory, 'quick' + + super filename, directory + end + + def cleanup + super + + quick_index_file = File.join(@directory, @filename) + compress quick_index_file + + # the complete quick index is in a directory, so move it as a whole + @files.delete quick_index_file + @files << @directory + end + + def add(spec) + @file.puts spec.full_name + add_yaml(spec) + add_marshal(spec) + end + + def add_yaml(spec) + fn = File.join @directory, "#{spec.full_name}.gemspec.rz" + zipped = zip spec.to_yaml + File.open fn, "wb" do |gsfile| gsfile.write zipped end + end + + def add_marshal(spec) + # HACK why does this not work in #initialize? + FileUtils.mkdir_p File.join(@directory, "Marshal.#{Gem.marshal_version}") + + fn = File.join @directory, "Marshal.#{Gem.marshal_version}", + "#{spec.full_name}.gemspec.rz" + + zipped = zip Marshal.dump(spec) + File.open fn, "wb" do |gsfile| gsfile.write zipped end + end + +end + diff --git a/lib/rubygems/install_update_options.rb b/lib/rubygems/install_update_options.rb new file mode 100644 index 0000000000..01c3a8af27 --- /dev/null +++ b/lib/rubygems/install_update_options.rb @@ -0,0 +1,87 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems' +require 'rubygems/security' + +## +# Mixin methods for install and update options for Gem::Commands +module Gem::InstallUpdateOptions + + # Add the install/update options to the option parser. + def add_install_update_options + OptionParser.accept Gem::Security::Policy do |value| + value = Gem::Security::Policies[value] + raise OptionParser::InvalidArgument, value if value.nil? + value + end + + add_option(:"Install/Update", '-i', '--install-dir DIR', + 'Gem repository directory to get installed', + 'gems') do |value, options| + options[:install_dir] = File.expand_path(value) + end + + add_option(:"Install/Update", '-d', '--[no-]rdoc', + 'Generate RDoc documentation for the gem on', + 'install') do |value, options| + options[:generate_rdoc] = value + end + + add_option(:"Install/Update", '--[no-]ri', + 'Generate RI documentation for the gem on', + 'install') do |value, options| + options[:generate_ri] = value + end + + add_option(:"Install/Update", '-E', '--env-shebang', + "Rewrite the shebang line on installed", + "scripts to use /usr/bin/env") do |value, options| + options[:env_shebang] = value + end + + add_option(:"Install/Update", '-f', '--[no-]force', + 'Force gem to install, bypassing dependency', + 'checks') do |value, options| + options[:force] = value + end + + add_option(:"Install/Update", '-t', '--[no-]test', + 'Run unit tests prior to installation') do |value, options| + options[:test] = value + end + + add_option(:"Install/Update", '-w', '--[no-]wrappers', + 'Use bin wrappers for executables', + 'Not available on dosish platforms') do |value, options| + options[:wrappers] = value + end + + add_option(:"Install/Update", '-P', '--trust-policy POLICY', + Gem::Security::Policy, + 'Specify gem trust policy') do |value, options| + options[:security_policy] = value + end + + add_option(:"Install/Update", '--ignore-dependencies', + 'Do not install any required dependent gems') do |value, options| + options[:ignore_dependencies] = value + end + + add_option(:"Install/Update", '-y', '--include-dependencies', + 'Unconditionally install the required', + 'dependent gems') do |value, options| + options[:include_dependencies] = value + end + end + + # Default options for the gem install command. + def install_update_defaults_str + '--rdoc --no-force --no-test --wrappers' + end + +end + diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb new file mode 100644 index 0000000000..03f7c92828 --- /dev/null +++ b/lib/rubygems/installer.rb @@ -0,0 +1,421 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'fileutils' +require 'pathname' +require 'rbconfig' + +require 'rubygems/format' +require 'rubygems/ext' + +## +# The installer class processes RubyGem .gem files and installs the +# files contained in the .gem into the Gem.path. +# +# Gem::Installer does the work of putting files in all the right places on the +# filesystem including unpacking the gem into its gem dir, installing the +# gemspec in the specifications dir, storing the cached gem in the cache dir, +# and installing either wrappers or symlinks for executables. +class Gem::Installer + + ## + # Raised when there is an error while building extensions. + # + class ExtensionBuildError < Gem::InstallError; end + + include Gem::UserInteraction + + ## + # Constructs an Installer instance that will install the gem located at + # +gem+. +options+ is a Hash with the following keys: + # + # :env_shebang:: Use /usr/bin/env in bin wrappers. + # :force:: Overrides all version checks and security policy checks, except + # for a signed-gems-only policy. + # :ignore_dependencies:: Don't raise if a dependency is missing. + # :install_dir:: The directory to install the gem into. + # :security_policy:: Use the specified security policy. See Gem::Security + # :wrappers:: Install wrappers if true, symlinks if false. + def initialize(gem, options={}) + @gem = gem + + options = { :force => false, :install_dir => Gem.dir }.merge options + + @env_shebang = options[:env_shebang] + @force = options[:force] + gem_home = options[:install_dir] + @gem_home = Pathname.new(gem_home).expand_path + @ignore_dependencies = options[:ignore_dependencies] + @security_policy = options[:security_policy] + @wrappers = options[:wrappers] + + begin + @format = Gem::Format.from_file_by_path @gem, @security_policy + rescue Gem::Package::FormatError + raise Gem::InstallError, "invalid gem format for #{@gem}" + end + + @spec = @format.spec + + @gem_dir = File.join(@gem_home, "gems", @spec.full_name).untaint + end + + ## + # Installs the gem and returns a loaded Gem::Specification for the installed + # gem. + # + # The gem will be installed with the following structure: + # + # @gem_home/ + # cache/.gem #=> a cached copy of the installed gem + # gems//... #=> extracted files + # specifications/.gemspec #=> the Gem::Specification + def install + # If we're forcing the install then disable security unless the security + # policy says that we only install singed gems. + @security_policy = nil if @force and @security_policy and + not @security_policy.only_signed + + unless @force then + if rrv = @spec.required_ruby_version then + unless rrv.satisfied_by? Gem::Version.new(RUBY_VERSION) then + raise Gem::InstallError, "#{@spec.name} requires Ruby version #{rrv}" + end + end + + if rrgv = @spec.required_rubygems_version then + unless rrgv.satisfied_by? Gem::Version.new(Gem::RubyGemsVersion) then + raise Gem::InstallError, + "#{@spec.name} requires RubyGems version #{rrgv}" + end + end + + unless @ignore_dependencies then + @spec.dependencies.each do |dep_gem| + ensure_dependency @spec, dep_gem + end + end + end + + FileUtils.mkdir_p @gem_home unless File.directory? @gem_home + raise Gem::FilePermissionError, @gem_home unless File.writable? @gem_home + + Gem.ensure_gem_subdirectories @gem_home + + FileUtils.mkdir_p @gem_dir + + extract_files + generate_bin + build_extensions + write_spec + + # HACK remove? Isn't this done in multiple places? + cached_gem = File.join @gem_home, "cache", @gem.split(/\//).pop + unless File.exist? cached_gem then + FileUtils.cp @gem, File.join(@gem_home, "cache") + end + + say @spec.post_install_message unless @spec.post_install_message.nil? + + @spec.loaded_from = File.join(@gem_home, 'specifications', + "#{@spec.full_name}.gemspec") + + return @spec + rescue Zlib::GzipFile::Error + raise Gem::InstallError, "gzip error installing #{@gem}" + end + + ## + # Ensure that the dependency is satisfied by the current installation of + # gem. If it is not an exception is raised. + # + # spec :: Gem::Specification + # dependency :: Gem::Dependency + def ensure_dependency(spec, dependency) + unless installation_satisfies_dependency? dependency then + raise Gem::InstallError, "#{spec.name} requires #{dependency}" + end + + true + end + + ## + # True if the current installed gems satisfy the given dependency. + # + # dependency :: Gem::Dependency + def installation_satisfies_dependency?(dependency) + current_index = Gem::SourceIndex.from_installed_gems + current_index.find_name(dependency.name, dependency.version_requirements).size > 0 + end + + ## + # Unpacks the gem into the given directory. + # + def unpack(directory) + @gem_dir = directory + @format = Gem::Format.from_file_by_path @gem, @security_policy + extract_files + end + + ## + # Writes the .gemspec specification (in Ruby) to the supplied + # spec_path. + # + # spec:: [Gem::Specification] The Gem specification to output + # spec_path:: [String] The location (path) to write the gemspec to + # + def write_spec + rubycode = @spec.to_ruby + + file_name = File.join @gem_home, 'specifications', + "#{@spec.full_name}.gemspec" + file_name.untaint + + File.open(file_name, "w") do |file| + file.puts rubycode + end + end + + ## + # Creates windows .bat files for easy running of commands + # + def generate_windows_script(bindir, filename) + if Gem.win_platform? then + script_name = filename + ".bat" + File.open(File.join(bindir, File.basename(script_name)), "w") do |file| + file.puts windows_stub_script(bindir, filename) + end + end + end + + def generate_bin + return if @spec.executables.nil? or @spec.executables.empty? + + # If the user has asked for the gem to be installed in a directory that is + # the system gem directory, then use the system bin directory, else create + # (or use) a new bin dir under the gem_home. + bindir = Gem.bindir @gem_home + + Dir.mkdir bindir unless File.exist? bindir + raise Gem::FilePermissionError.new(bindir) unless File.writable? bindir + + @spec.executables.each do |filename| + filename.untaint + bin_path = File.join @gem_dir, 'bin', filename + mode = File.stat(bin_path).mode | 0111 + File.chmod mode, bin_path + + if @wrappers then + generate_bin_script filename, bindir + else + generate_bin_symlink filename, bindir + end + end + end + + ## + # Creates the scripts to run the applications in the gem. + #-- + # The Windows script is generated in addition to the regular one due to a + # bug or misfeature in the Windows shell's pipe. See + # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/193379 + # + def generate_bin_script(filename, bindir) + File.open(File.join(bindir, File.basename(filename)), "w", 0755) do |file| + file.print app_script_text(filename) + end + generate_windows_script bindir, filename + end + + ## + # Creates the symlinks to run the applications in the gem. Moves + # the symlink if the gem being installed has a newer version. + # + def generate_bin_symlink(filename, bindir) + if Config::CONFIG["arch"] =~ /dos|win32/i then + alert_warning "Unable to use symlinks on win32, installing wrapper" + generate_bin_script filename, bindir + return + end + + src = File.join @gem_dir, 'bin', filename + dst = File.join bindir, File.basename(filename) + + if File.exist? dst then + if File.symlink? dst then + link = File.readlink(dst).split File::SEPARATOR + cur_version = Gem::Version.create(link[-3].sub(/^.*-/, '')) + return if @spec.version < cur_version + end + File.unlink dst + end + + File.symlink src, dst + end + + ## + # Generates a #! line for +bin_file_name+'s wrapper copying arguments if + # necessary. + def shebang(bin_file_name) + if @env_shebang then + "#!/usr/bin/env ruby" + else + path = File.join @gem_dir, @spec.bindir, bin_file_name + + File.open(path, "rb") do |file| + first_line = file.gets + if first_line =~ /^#!/ then + # Preserve extra words on shebang line, like "-w". Thanks RPA. + shebang = first_line.sub(/\A\#!.*?ruby\S*/, "#!#{Gem.ruby}") + else + # Create a plain shebang line. + shebang = "#!#{Gem.ruby}" + end + + shebang.strip # Avoid nasty ^M issues. + end + end + end + + # Return the text for an application file. + def app_script_text(bin_file_name) + <<-TEXT +#{shebang bin_file_name} +# +# This file was generated by RubyGems. +# +# The application '#{@spec.name}' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'rubygems' + +version = "#{Gem::Requirement.default}" + +if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then + version = $1 + ARGV.shift +end + +gem '#{@spec.name}', version +load '#{bin_file_name}' +TEXT + end + + # return the stub script text used to launch the true ruby script + def windows_stub_script(bindir, bin_file_name) + <<-TEXT +@ECHO OFF +IF NOT "%~f0" == "~f0" GOTO :WinNT +@"#{Gem.ruby}" "#{File.join(bindir, bin_file_name)}" %1 %2 %3 %4 %5 %6 %7 %8 %9 +GOTO :EOF +:WinNT +"%~dp0ruby.exe" "%~dpn0" %* +TEXT + end + + # Builds extensions. Valid types of extensions are extconf.rb files, + # configure scripts and rakefiles or mkrf_conf files. + def build_extensions + return if @spec.extensions.empty? + say "Building native extensions. This could take a while..." + start_dir = Dir.pwd + dest_path = File.join @gem_dir, @spec.require_paths.first + ran_rake = false # only run rake once + + @spec.extensions.each do |extension| + break if ran_rake + results = [] + + builder = case extension + when /extconf/ then + Gem::Ext::ExtConfBuilder + when /configure/ then + Gem::Ext::ConfigureBuilder + when /rakefile/i, /mkrf_conf/i then + ran_rake = true + Gem::Ext::RakeBuilder + else + results = ["No builder for extension '#{extension}'"] + nil + end + + begin + Dir.chdir File.join(@gem_dir, File.dirname(extension)) + results = builder.build(extension, @gem_dir, dest_path, results) + rescue => ex + results = results.join "\n" + + File.open('gem_make.out', 'wb') { |f| f.puts results } + + message = <<-EOF +ERROR: Failed to build gem native extension. + +#{results} + +Gem files will remain installed in #{@gem_dir} for inspection. +Results logged to #{File.join(Dir.pwd, 'gem_make.out')} + EOF + + raise ExtensionBuildError, message + ensure + Dir.chdir start_dir + end + end + end + + ## + # Reads the file index and extracts each file into the gem directory. + # + # Ensures that files can't be installed outside the gem directory. + def extract_files + expand_and_validate_gem_dir + + raise ArgumentError, "format required to extract from" if @format.nil? + + @format.file_entries.each do |entry, file_data| + path = entry['path'].untaint + + if path =~ /\A\// then # for extra sanity + raise Gem::InstallError, + "attempt to install file into #{entry['path'].inspect}" + end + + path = File.expand_path File.join(@gem_dir, path) + + if path !~ /\A#{Regexp.escape @gem_dir}/ then + msg = "attempt to install file into %p under %p" % + [entry['path'], @gem_dir] + raise Gem::InstallError, msg + end + + FileUtils.mkdir_p File.dirname(path) + + File.open(path, "wb") do |out| + out.write file_data + end + end + end + + private + + # HACK Pathname is broken on windows. + def absolute_path? pathname + pathname.absolute? or (Gem.win_platform? and pathname.to_s =~ /\A[a-z]:/i) + end + + def expand_and_validate_gem_dir + @gem_dir = Pathname.new(@gem_dir).expand_path + + unless absolute_path?(@gem_dir) then # HACK is this possible after #expand_path? + raise ArgumentError, "install directory %p not absolute" % @gem_dir + end + + @gem_dir = @gem_dir.to_s + end + +end + diff --git a/lib/rubygems/local_remote_options.rb b/lib/rubygems/local_remote_options.rb new file mode 100644 index 0000000000..1a5410bef7 --- /dev/null +++ b/lib/rubygems/local_remote_options.rb @@ -0,0 +1,106 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems' + +# Mixin methods for local and remote Gem::Command options. +module Gem::LocalRemoteOptions + + # Allows OptionParser to handle HTTP URIs. + def accept_uri_http + OptionParser.accept URI::HTTP do |value| + begin + value = URI.parse value + rescue URI::InvalidURIError + raise OptionParser::InvalidArgument, value + end + + raise OptionParser::InvalidArgument, value unless value.scheme == 'http' + + value + end + end + + # Add local/remote options to the command line parser. + def add_local_remote_options + add_option(:"Local/Remote", '-l', '--local', + 'Restrict operations to the LOCAL domain') do |value, options| + options[:domain] = :local + end + + add_option(:"Local/Remote", '-r', '--remote', + 'Restrict operations to the REMOTE domain') do |value, options| + options[:domain] = :remote + end + + add_option(:"Local/Remote", '-b', '--both', + 'Allow LOCAL and REMOTE operations') do |value, options| + options[:domain] = :both + end + + add_bulk_threshold_option + add_source_option + add_proxy_option + add_update_sources_option + end + + # Add the --bulk-threshold option + def add_bulk_threshold_option + add_option(:"Local/Remote", '-B', '--bulk-threshold COUNT', + "Threshold for switching to bulk", + "synchronization (default #{Gem.configuration.bulk_threshold})") do + |value, options| + Gem.configuration.bulk_threshold = value.to_i + end + end + + # Add the --http-proxy option + def add_proxy_option + accept_uri_http + + add_option(:"Local/Remote", '-p', '--[no-]http-proxy [URL]', URI::HTTP, + 'Use HTTP proxy for remote operations') do |value, options| + options[:http_proxy] = (value == false) ? :no_proxy : value + Gem.configuration[:http_proxy] = options[:http_proxy] + end + end + + # Add the --source option + def add_source_option + accept_uri_http + + add_option(:"Local/Remote", '--source URL', URI::HTTP, + 'Use URL as the remote source for gems') do |value, options| + if options[:added_source] then + Gem.sources << value + else + options[:added_source] = true + Gem.sources.replace [value] + end + end + end + + # Add the --source option + def add_update_sources_option + + add_option(:"Local/Remote", '-u', '--[no-]update-sources', + 'Update local source cache') do |value, options| + Gem.configuration.update_sources = value + end + end + + # Is local fetching enabled? + def local? + options[:domain] == :local || options[:domain] == :both + end + + # Is remote fetching enabled? + def remote? + options[:domain] == :remote || options[:domain] == :both + end + +end + diff --git a/lib/rubygems/old_format.rb b/lib/rubygems/old_format.rb new file mode 100644 index 0000000000..ef5d621f52 --- /dev/null +++ b/lib/rubygems/old_format.rb @@ -0,0 +1,148 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'fileutils' +require 'yaml' +require 'zlib' + +module Gem + + ## + # The format class knows the guts of the RubyGem .gem file format + # and provides the capability to read gem files + # + class OldFormat + attr_accessor :spec, :file_entries, :gem_path + + ## + # Constructs an instance of a Format object, representing the gem's + # data structure. + # + # gem:: [String] The file name of the gem + # + def initialize(gem_path) + @gem_path = gem_path + end + + ## + # Reads the named gem file and returns a Format object, representing + # the data from the gem file + # + # file_path:: [String] Path to the gem file + # + def self.from_file_by_path(file_path) + unless File.exist?(file_path) + raise Gem::Exception, "Cannot load gem file [#{file_path}]" + end + File.open(file_path, 'rb') do |file| + from_io(file, file_path) + end + end + + ## + # Reads a gem from an io stream and returns a Format object, representing + # the data from the gem file + # + # io:: [IO] Stream from which to read the gem + # + def self.from_io(io, gem_path="(io)") + format = self.new(gem_path) + skip_ruby(io) + format.spec = read_spec(io) + format.file_entries = [] + read_files_from_gem(io) do |entry, file_data| + format.file_entries << [entry, file_data] + end + format + end + + private + ## + # Skips the Ruby self-install header. After calling this method, the + # IO index will be set after the Ruby code. + # + # file:: [IO] The IO to process (skip the Ruby code) + # + def self.skip_ruby(file) + end_seen = false + loop { + line = file.gets + if(line == nil || line.chomp == "__END__") then + end_seen = true + break + end + } + if(end_seen == false) then + raise Gem::Exception.new("Failed to find end of ruby script while reading gem") + end + end + + ## + # Reads the specification YAML from the supplied IO and constructs + # a Gem::Specification from it. After calling this method, the + # IO index will be set after the specification header. + # + # file:: [IO] The IO to process + # + def self.read_spec(file) + yaml = '' + begin + read_until_dashes(file) do |line| + yaml << line + end + Specification.from_yaml(yaml) + rescue YAML::Error => e + raise Gem::Exception.new("Failed to parse gem specification out of gem file") + rescue ArgumentError => e + raise Gem::Exception.new("Failed to parse gem specification out of gem file") + end + end + + ## + # Reads lines from the supplied IO until a end-of-yaml (---) is + # reached + # + # file:: [IO] The IO to process + # block:: [String] The read line + # + def self.read_until_dashes(file) + while((line = file.gets) && line.chomp.strip != "---") do + yield line + end + end + + + ## + # Reads the embedded file data from a gem file, yielding an entry + # containing metadata about the file and the file contents themselves + # for each file that's archived in the gem. + # NOTE: Many of these methods should be extracted into some kind of + # Gem file read/writer + # + # gem_file:: [IO] The IO to process + # + def self.read_files_from_gem(gem_file) + errstr = "Error reading files from gem" + header_yaml = '' + begin + self.read_until_dashes(gem_file) do |line| + header_yaml << line + end + header = YAML.load(header_yaml) + raise Gem::Exception.new(errstr) unless header + header.each do |entry| + file_data = '' + self.read_until_dashes(gem_file) do |line| + file_data << line + end + yield [entry, Zlib::Inflate.inflate(file_data.strip.unpack("m")[0])] + end + rescue Exception,Zlib::DataError => e + raise Gem::Exception.new(errstr) + end + end + end +end diff --git a/lib/rubygems/open-uri.rb b/lib/rubygems/open-uri.rb new file mode 100644 index 0000000000..ffc8e48571 --- /dev/null +++ b/lib/rubygems/open-uri.rb @@ -0,0 +1,773 @@ +require 'uri' +require 'stringio' +require 'time' + +# :stopdoc: +module Kernel + private + alias rubygems_open_uri_original_open open # :nodoc: + + # makes possible to open various resources including URIs. + # If the first argument respond to `open' method, + # the method is called with the rest arguments. + # + # If the first argument is a string which begins with xxx://, + # it is parsed by URI.parse. If the parsed object respond to `open' method, + # the method is called with the rest arguments. + # + # Otherwise original open is called. + # + # Since open-uri.rb provides URI::HTTP#open, URI::HTTPS#open and + # URI::FTP#open, + # Kernel[#.]open can accepts such URIs and strings which begins with + # http://, https:// and ftp://. + # In these case, the opened file object is extended by OpenURI::Meta. + def open(name, *rest, &block) # :doc: + if name.respond_to?(:open) + name.open(*rest, &block) + elsif name.respond_to?(:to_str) && + %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ name && + (uri = URI.parse(name)).respond_to?(:open) + uri.open(*rest, &block) + else + rubygems_open_uri_original_open(name, *rest, &block) + end + end + module_function :open +end + +# OpenURI is an easy-to-use wrapper for net/http, net/https and net/ftp. +# +#== Example +# +# It is possible to open http/https/ftp URL as usual like opening a file: +# +# open("http://www.ruby-lang.org/") {|f| +# f.each_line {|line| p line} +# } +# +# The opened file has several methods for meta information as follows since +# it is extended by OpenURI::Meta. +# +# open("http://www.ruby-lang.org/en") {|f| +# f.each_line {|line| p line} +# p f.base_uri # +# p f.content_type # "text/html" +# p f.charset # "iso-8859-1" +# p f.content_encoding # [] +# p f.last_modified # Thu Dec 05 02:45:02 UTC 2002 +# } +# +# Additional header fields can be specified by an optional hash argument. +# +# open("http://www.ruby-lang.org/en/", +# "User-Agent" => "Ruby/#{RUBY_VERSION}", +# "From" => "foo@bar.invalid", +# "Referer" => "http://www.ruby-lang.org/") {|f| +# # ... +# } +# +# The environment variables such as http_proxy, https_proxy and ftp_proxy +# are in effect by default. :proxy => nil disables proxy. +# +# open("http://www.ruby-lang.org/en/raa.html", :proxy => nil) {|f| +# # ... +# } +# +# URI objects can be opened in a similar way. +# +# uri = URI.parse("http://www.ruby-lang.org/en/") +# uri.open {|f| +# # ... +# } +# +# URI objects can be read directly. The returned string is also extended by +# OpenURI::Meta. +# +# str = uri.read +# p str.base_uri +# +# Author:: Tanaka Akira + +module OpenURI + Options = { + :proxy => true, + :proxy_http_basic_authentication => true, + :progress_proc => true, + :content_length_proc => true, + :http_basic_authentication => true, + :read_timeout => true, + :ssl_ca_cert => nil, + :ssl_verify_mode => nil, + } + + def OpenURI.check_options(options) # :nodoc: + options.each {|k, v| + next unless Symbol === k + unless Options.include? k + raise ArgumentError, "unrecognized option: #{k}" + end + } + end + + def OpenURI.scan_open_optional_arguments(*rest) # :nodoc: + if !rest.empty? && (String === rest.first || Integer === rest.first) + mode = rest.shift + if !rest.empty? && Integer === rest.first + perm = rest.shift + end + end + return mode, perm, rest + end + + def OpenURI.open_uri(name, *rest) # :nodoc: + uri = URI::Generic === name ? name : URI.parse(name) + mode, perm, rest = OpenURI.scan_open_optional_arguments(*rest) + options = rest.shift if !rest.empty? && Hash === rest.first + raise ArgumentError.new("extra arguments") if !rest.empty? + options ||= {} + OpenURI.check_options(options) + + unless mode == nil || + mode == 'r' || mode == 'rb' || + mode == File::RDONLY + raise ArgumentError.new("invalid access mode #{mode} (#{uri.class} resource is read only.)") + end + + io = open_loop(uri, options) + if block_given? + begin + yield io + ensure + io.close + end + else + io + end + end + + def OpenURI.open_loop(uri, options) # :nodoc: + proxy_opts = [] + proxy_opts << :proxy_http_basic_authentication if options.include? :proxy_http_basic_authentication + proxy_opts << :proxy if options.include? :proxy + proxy_opts.compact! + if 1 < proxy_opts.length + raise ArgumentError, "multiple proxy options specified" + end + case proxy_opts.first + when :proxy_http_basic_authentication + opt_proxy, proxy_user, proxy_pass = options.fetch(:proxy_http_basic_authentication) + proxy_user = proxy_user.to_str + proxy_pass = proxy_pass.to_str + if opt_proxy == true + raise ArgumentError.new("Invalid authenticated proxy option: #{options[:proxy_http_basic_authentication].inspect}") + end + when :proxy + opt_proxy = options.fetch(:proxy) + proxy_user = nil + proxy_pass = nil + when nil + opt_proxy = true + proxy_user = nil + proxy_pass = nil + end + case opt_proxy + when true + find_proxy = lambda {|u| pxy = u.find_proxy; pxy ? [pxy, nil, nil] : nil} + when nil, false + find_proxy = lambda {|u| nil} + when String + opt_proxy = URI.parse(opt_proxy) + find_proxy = lambda {|u| [opt_proxy, proxy_user, proxy_pass]} + when URI::Generic + find_proxy = lambda {|u| [opt_proxy, proxy_user, proxy_pass]} + else + raise ArgumentError.new("Invalid proxy option: #{opt_proxy}") + end + + uri_set = {} + buf = nil + while true + redirect = catch(:open_uri_redirect) { + buf = Buffer.new + uri.buffer_open(buf, find_proxy.call(uri), options) + nil + } + if redirect + if redirect.relative? + # Although it violates RFC2616, Location: field may have relative + # URI. It is converted to absolute URI using uri as a base URI. + redirect = uri + redirect + end + unless OpenURI.redirectable?(uri, redirect) + raise "redirection forbidden: #{uri} -> #{redirect}" + end + if options.include? :http_basic_authentication + # send authentication only for the URI directly specified. + options = options.dup + options.delete :http_basic_authentication + end + uri = redirect + raise "HTTP redirection loop: #{uri}" if uri_set.include? uri.to_s + uri_set[uri.to_s] = true + else + break + end + end + io = buf.io + io.base_uri = uri + io + end + + def OpenURI.redirectable?(uri1, uri2) # :nodoc: + # This test is intended to forbid a redirection from http://... to + # file:///etc/passwd. + # However this is ad hoc. It should be extensible/configurable. + uri1.scheme.downcase == uri2.scheme.downcase || + (/\A(?:http|ftp)\z/i =~ uri1.scheme && /\A(?:http|ftp)\z/i =~ uri2.scheme) + end + + def OpenURI.open_http(buf, target, proxy, options) # :nodoc: + if proxy + proxy_uri, proxy_user, proxy_pass = proxy + raise "Non-HTTP proxy URI: #{proxy_uri}" if proxy_uri.class != URI::HTTP + end + + if target.userinfo && "1.9.0" <= RUBY_VERSION + # don't raise for 1.8 because compatibility. + raise ArgumentError, "userinfo not supported. [RFC3986]" + end + + header = {} + options.each {|k, v| header[k] = v if String === k } + + require 'net/http' + klass = Net::HTTP + if URI::HTTP === target + # HTTP or HTTPS + if proxy + if proxy_user && proxy_pass + klass = Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_user, proxy_pass) + else + klass = Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port) + end + end + target_host = target.host + target_port = target.port + request_uri = target.request_uri + else + # FTP over HTTP proxy + target_host = proxy_uri.host + target_port = proxy_uri.port + request_uri = target.to_s + if proxy_user && proxy_pass + header["Proxy-Authorization"] = 'Basic ' + ["#{proxy_user}:#{proxy_pass}"].pack('m').delete("\r\n") + end + end + + http = klass.new(target_host, target_port) + if target.class == URI::HTTPS + require 'net/https' + http.use_ssl = true + http.verify_mode = options[:ssl_verify_mode] || OpenSSL::SSL::VERIFY_PEER + store = OpenSSL::X509::Store.new + if options[:ssl_ca_cert] + if File.directory? options[:ssl_ca_cert] + store.add_path options[:ssl_ca_cert] + else + store.add_file options[:ssl_ca_cert] + end + else + store.set_default_paths + end + store.set_default_paths + http.cert_store = store + end + if options.include? :read_timeout + http.read_timeout = options[:read_timeout] + end + + resp = nil + http.start { + if target.class == URI::HTTPS + # xxx: information hiding violation + sock = http.instance_variable_get(:@socket) + if sock.respond_to?(:io) + sock = sock.io # 1.9 + else + sock = sock.instance_variable_get(:@socket) # 1.8 + end + sock.post_connection_check(target_host) + end + req = Net::HTTP::Get.new(request_uri, header) + if options.include? :http_basic_authentication + user, pass = options[:http_basic_authentication] + req.basic_auth user, pass + end + http.request(req) {|response| + resp = response + if options[:content_length_proc] && Net::HTTPSuccess === resp + if resp.key?('Content-Length') + options[:content_length_proc].call(resp['Content-Length'].to_i) + else + options[:content_length_proc].call(nil) + end + end + resp.read_body {|str| + buf << str + if options[:progress_proc] && Net::HTTPSuccess === resp + options[:progress_proc].call(buf.size) + end + } + } + } + io = buf.io + io.rewind + io.status = [resp.code, resp.message] + resp.each {|name,value| buf.io.meta_add_field name, value } + case resp + when Net::HTTPSuccess + when Net::HTTPMovedPermanently, # 301 + Net::HTTPFound, # 302 + Net::HTTPSeeOther, # 303 + Net::HTTPTemporaryRedirect # 307 + throw :open_uri_redirect, URI.parse(resp['location']) + else + raise OpenURI::HTTPError.new(io.status.join(' '), io) + end + end + + class HTTPError < StandardError + def initialize(message, io) + super(message) + @io = io + end + attr_reader :io + end + + class Buffer # :nodoc: + def initialize + @io = StringIO.new + @size = 0 + end + attr_reader :size + + StringMax = 10240 + def <<(str) + @io << str + @size += str.length + if StringIO === @io && StringMax < @size + require 'tempfile' + io = Tempfile.new('open-uri') + io.binmode + Meta.init io, @io if @io.respond_to? :meta + io << @io.string + @io = io + end + end + + def io + Meta.init @io unless @io.respond_to? :meta + @io + end + end + + # Mixin for holding meta-information. + module Meta + def Meta.init(obj, src=nil) # :nodoc: + obj.extend Meta + obj.instance_eval { + @base_uri = nil + @meta = {} + } + if src + obj.status = src.status + obj.base_uri = src.base_uri + src.meta.each {|name, value| + obj.meta_add_field(name, value) + } + end + end + + # returns an Array which consists status code and message. + attr_accessor :status + + # returns a URI which is base of relative URIs in the data. + # It may differ from the URI supplied by a user because redirection. + attr_accessor :base_uri + + # returns a Hash which represents header fields. + # The Hash keys are downcased for canonicalization. + attr_reader :meta + + def meta_add_field(name, value) # :nodoc: + @meta[name.downcase] = value + end + + # returns a Time which represents Last-Modified field. + def last_modified + if v = @meta['last-modified'] + Time.httpdate(v) + else + nil + end + end + + RE_LWS = /[\r\n\t ]+/n + RE_TOKEN = %r{[^\x00- ()<>@,;:\\"/\[\]?={}\x7f]+}n + RE_QUOTED_STRING = %r{"(?:[\r\n\t !#-\[\]-~\x80-\xff]|\\[\x00-\x7f])*"}n + RE_PARAMETERS = %r{(?:;#{RE_LWS}?#{RE_TOKEN}#{RE_LWS}?=#{RE_LWS}?(?:#{RE_TOKEN}|#{RE_QUOTED_STRING})#{RE_LWS}?)*}n + + def content_type_parse # :nodoc: + v = @meta['content-type'] + # The last (?:;#{RE_LWS}?)? matches extra ";" which violates RFC2045. + if v && %r{\A#{RE_LWS}?(#{RE_TOKEN})#{RE_LWS}?/(#{RE_TOKEN})#{RE_LWS}?(#{RE_PARAMETERS})(?:;#{RE_LWS}?)?\z}no =~ v + type = $1.downcase + subtype = $2.downcase + parameters = [] + $3.scan(/;#{RE_LWS}?(#{RE_TOKEN})#{RE_LWS}?=#{RE_LWS}?(?:(#{RE_TOKEN})|(#{RE_QUOTED_STRING}))/no) {|att, val, qval| + val = qval.gsub(/[\r\n\t !#-\[\]-~\x80-\xff]+|(\\[\x00-\x7f])/) { $1 ? $1[1,1] : $& } if qval + parameters << [att.downcase, val] + } + ["#{type}/#{subtype}", *parameters] + else + nil + end + end + + # returns "type/subtype" which is MIME Content-Type. + # It is downcased for canonicalization. + # Content-Type parameters are stripped. + def content_type + type, *parameters = content_type_parse + type || 'application/octet-stream' + end + + # returns a charset parameter in Content-Type field. + # It is downcased for canonicalization. + # + # If charset parameter is not given but a block is given, + # the block is called and its result is returned. + # It can be used to guess charset. + # + # If charset parameter and block is not given, + # nil is returned except text type in HTTP. + # In that case, "iso-8859-1" is returned as defined by RFC2616 3.7.1. + def charset + type, *parameters = content_type_parse + if pair = parameters.assoc('charset') + pair.last.downcase + elsif block_given? + yield + elsif type && %r{\Atext/} =~ type && + @base_uri && /\Ahttp\z/i =~ @base_uri.scheme + "iso-8859-1" # RFC2616 3.7.1 + else + nil + end + end + + # returns a list of encodings in Content-Encoding field + # as an Array of String. + # The encodings are downcased for canonicalization. + def content_encoding + v = @meta['content-encoding'] + if v && %r{\A#{RE_LWS}?#{RE_TOKEN}#{RE_LWS}?(?:,#{RE_LWS}?#{RE_TOKEN}#{RE_LWS}?)*}o =~ v + v.scan(RE_TOKEN).map {|content_coding| content_coding.downcase} + else + [] + end + end + end + + # Mixin for HTTP and FTP URIs. + module OpenRead + # OpenURI::OpenRead#open provides `open' for URI::HTTP and URI::FTP. + # + # OpenURI::OpenRead#open takes optional 3 arguments as: + # OpenURI::OpenRead#open([mode [, perm]] [, options]) [{|io| ... }] + # + # `mode', `perm' is same as Kernel#open. + # + # However, `mode' must be read mode because OpenURI::OpenRead#open doesn't + # support write mode (yet). + # Also `perm' is just ignored because it is meaningful only for file + # creation. + # + # `options' must be a hash. + # + # Each pairs which key is a string in the hash specify a extra header + # field for HTTP. + # I.e. it is ignored for FTP without HTTP proxy. + # + # The hash may include other options which key is a symbol: + # + # [:proxy] + # Synopsis: + # :proxy => "http://proxy.foo.com:8000/" + # :proxy => URI.parse("http://proxy.foo.com:8000/") + # :proxy => true + # :proxy => false + # :proxy => nil + # + # If :proxy option is specified, the value should be String, URI, + # boolean or nil. + # When String or URI is given, it is treated as proxy URI. + # When true is given or the option itself is not specified, + # environment variable `scheme_proxy' is examined. + # `scheme' is replaced by `http', `https' or `ftp'. + # When false or nil is given, the environment variables are ignored and + # connection will be made to a server directly. + # + # [:proxy_http_basic_authentication] + # Synopsis: + # :proxy_http_basic_authentication => ["http://proxy.foo.com:8000/", "proxy-user", "proxy-password"] + # :proxy_http_basic_authentication => [URI.parse("http://proxy.foo.com:8000/"), "proxy-user", "proxy-password"] + # + # If :proxy option is specified, the value should be an Array with 3 elements. + # It should contain a proxy URI, a proxy user name and a proxy password. + # The proxy URI should be a String, an URI or nil. + # The proxy user name and password should be a String. + # + # If nil is given for the proxy URI, this option is just ignored. + # + # If :proxy and :proxy_http_basic_authentication is specified, + # ArgumentError is raised. + # + # [:http_basic_authentication] + # Synopsis: + # :http_basic_authentication=>[user, password] + # + # If :http_basic_authentication is specified, + # the value should be an array which contains 2 strings: + # username and password. + # It is used for HTTP Basic authentication defined by RFC 2617. + # + # [:content_length_proc] + # Synopsis: + # :content_length_proc => lambda {|content_length| ... } + # + # If :content_length_proc option is specified, the option value procedure + # is called before actual transfer is started. + # It takes one argument which is expected content length in bytes. + # + # If two or more transfer is done by HTTP redirection, the procedure + # is called only one for a last transfer. + # + # When expected content length is unknown, the procedure is called with + # nil. + # It is happen when HTTP response has no Content-Length header. + # + # [:progress_proc] + # Synopsis: + # :progress_proc => lambda {|size| ...} + # + # If :progress_proc option is specified, the proc is called with one + # argument each time when `open' gets content fragment from network. + # The argument `size' `size' is a accumulated transfered size in bytes. + # + # If two or more transfer is done by HTTP redirection, the procedure + # is called only one for a last transfer. + # + # :progress_proc and :content_length_proc are intended to be used for + # progress bar. + # For example, it can be implemented as follows using Ruby/ProgressBar. + # + # pbar = nil + # open("http://...", + # :content_length_proc => lambda {|t| + # if t && 0 < t + # pbar = ProgressBar.new("...", t) + # pbar.file_transfer_mode + # end + # }, + # :progress_proc => lambda {|s| + # pbar.set s if pbar + # }) {|f| ... } + # + # [:read_timeout] + # Synopsis: + # :read_timeout=>nil (no timeout) + # :read_timeout=>10 (10 second) + # + # :read_timeout option specifies a timeout of read for http connections. + # + # [:ssl_ca_cert] + # Synopsis: + # :ssl_ca_cert=>filename + # + # :ssl_ca_cert is used to specify CA certificate for SSL. + # If it is given, default certificates are not used. + # + # [:ssl_verify_mode] + # Synopsis: + # :ssl_verify_mode=>mode + # + # :ssl_verify_mode is used to specify openssl verify mode. + # + # OpenURI::OpenRead#open returns an IO like object if block is not given. + # Otherwise it yields the IO object and return the value of the block. + # The IO object is extended with OpenURI::Meta. + def open(*rest, &block) + OpenURI.open_uri(self, *rest, &block) + end + + # OpenURI::OpenRead#read([options]) reads a content referenced by self and + # returns the content as string. + # The string is extended with OpenURI::Meta. + # The argument `options' is same as OpenURI::OpenRead#open. + def read(options={}) + self.open(options) {|f| + str = f.read + Meta.init str, f + str + } + end + end +end + +module URI + class Generic + # returns a proxy URI. + # The proxy URI is obtained from environment variables such as http_proxy, + # ftp_proxy, no_proxy, etc. + # If there is no proper proxy, nil is returned. + # + # Note that capitalized variables (HTTP_PROXY, FTP_PROXY, NO_PROXY, etc.) + # are examined too. + # + # But http_proxy and HTTP_PROXY is treated specially under CGI environment. + # It's because HTTP_PROXY may be set by Proxy: header. + # So HTTP_PROXY is not used. + # http_proxy is not used too if the variable is case insensitive. + # CGI_HTTP_PROXY can be used instead. + def find_proxy + name = self.scheme.downcase + '_proxy' + proxy_uri = nil + if name == 'http_proxy' && ENV.include?('REQUEST_METHOD') # CGI? + # HTTP_PROXY conflicts with *_proxy for proxy settings and + # HTTP_* for header information in CGI. + # So it should be careful to use it. + pairs = ENV.reject {|k, v| /\Ahttp_proxy\z/i !~ k } + case pairs.length + when 0 # no proxy setting anyway. + proxy_uri = nil + when 1 + k, v = pairs.shift + if k == 'http_proxy' && ENV[k.upcase] == nil + # http_proxy is safe to use because ENV is case sensitive. + proxy_uri = ENV[name] + else + proxy_uri = nil + end + else # http_proxy is safe to use because ENV is case sensitive. + proxy_uri = ENV[name] + end + if !proxy_uri + # Use CGI_HTTP_PROXY. cf. libwww-perl. + proxy_uri = ENV["CGI_#{name.upcase}"] + end + elsif name == 'http_proxy' + unless proxy_uri = ENV[name] + if proxy_uri = ENV[name.upcase] + warn 'The environment variable HTTP_PROXY is discouraged. Use http_proxy.' + end + end + else + proxy_uri = ENV[name] || ENV[name.upcase] + end + + if proxy_uri && self.host + require 'socket' + begin + addr = IPSocket.getaddress(self.host) + proxy_uri = nil if /\A127\.|\A::1\z/ =~ addr + rescue SocketError + end + end + + if proxy_uri + proxy_uri = URI.parse(proxy_uri) + name = 'no_proxy' + if no_proxy = ENV[name] || ENV[name.upcase] + no_proxy.scan(/([^:,]*)(?::(\d+))?/) {|host, port| + if /(\A|\.)#{Regexp.quote host}\z/i =~ self.host && + (!port || self.port == port.to_i) + proxy_uri = nil + break + end + } + end + proxy_uri + else + nil + end + end + end + + class HTTP + def buffer_open(buf, proxy, options) # :nodoc: + OpenURI.open_http(buf, self, proxy, options) + end + + include OpenURI::OpenRead + end + + class FTP + def buffer_open(buf, proxy, options) # :nodoc: + if proxy + OpenURI.open_http(buf, self, proxy, options) + return + end + require 'net/ftp' + + directories = self.path.split(%r{/}, -1) + directories.shift if directories[0] == '' # strip a field before leading slash + directories.each {|d| + d.gsub!(/%([0-9A-Fa-f][0-9A-Fa-f])/) { [$1].pack("H2") } + } + unless filename = directories.pop + raise ArgumentError, "no filename: #{self.inspect}" + end + directories.each {|d| + if /[\r\n]/ =~ d + raise ArgumentError, "invalid directory: #{d.inspect}" + end + } + if /[\r\n]/ =~ filename + raise ArgumentError, "invalid filename: #{filename.inspect}" + end + typecode = self.typecode + if typecode && /\A[aid]\z/ !~ typecode + raise ArgumentError, "invalid typecode: #{typecode.inspect}" + end + + # The access sequence is defined by RFC 1738 + ftp = Net::FTP.open(self.host) + # todo: extract user/passwd from .netrc. + user = 'anonymous' + passwd = nil + user, passwd = self.userinfo.split(/:/) if self.userinfo + ftp.login(user, passwd) + directories.each {|cwd| + ftp.voidcmd("CWD #{cwd}") + } + if typecode + # xxx: typecode D is not handled. + ftp.voidcmd("TYPE #{typecode.upcase}") + end + if options[:content_length_proc] + options[:content_length_proc].call(ftp.size(filename)) + end + ftp.retrbinary("RETR #{filename}", 4096) { |str| + buf << str + options[:progress_proc].call(buf.size) if options[:progress_proc] + } + ftp.close + buf.io.rewind + end + + include OpenURI::OpenRead + end +end +# :startdoc: diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb new file mode 100644 index 0000000000..fd75d188bd --- /dev/null +++ b/lib/rubygems/package.rb @@ -0,0 +1,851 @@ +#++ +# Copyright (C) 2004 Mauricio Julio Fernández Pradier +# See LICENSE.txt for additional licensing information. +#-- + +require 'fileutils' +require 'find' +require 'stringio' +require 'yaml' +require 'zlib' + +require 'rubygems/digest/md5' +require 'rubygems/security' +require 'rubygems/specification' + +# Wrapper for FileUtils meant to provide logging and additional operations if +# needed. +class Gem::FileOperations + + def initialize(logger = nil) + @logger = logger + end + + def method_missing(meth, *args, &block) + case + when FileUtils.respond_to?(meth) + @logger.log "#{meth}: #{args}" if @logger + FileUtils.send meth, *args, &block + when Gem::FileOperations.respond_to?(meth) + @logger.log "#{meth}: #{args}" if @logger + Gem::FileOperations.send meth, *args, &block + else + super + end + end + +end + +module Gem::Package + + class Error < StandardError; end + class NonSeekableIO < Error; end + class ClosedIO < Error; end + class BadCheckSum < Error; end + class TooLongFileName < Error; end + class FormatError < Error; end + + module FSyncDir + private + def fsync_dir(dirname) + # make sure this hits the disc + begin + dir = open(dirname, "r") + dir.fsync + rescue # ignore IOError if it's an unpatched (old) Ruby + ensure + dir.close if dir rescue nil + end + end + end + + class TarHeader + FIELDS = [:name, :mode, :uid, :gid, :size, :mtime, :checksum, :typeflag, + :linkname, :magic, :version, :uname, :gname, :devmajor, + :devminor, :prefix] + FIELDS.each {|x| attr_reader x} + + def self.new_from_stream(stream) + data = stream.read(512) + fields = data.unpack("A100" + # record name + "A8A8A8" + # mode, uid, gid + "A12A12" + # size, mtime + "A8A" + # checksum, typeflag + "A100" + # linkname + "A6A2" + # magic, version + "A32" + # uname + "A32" + # gname + "A8A8" + # devmajor, devminor + "A155") # prefix + name = fields.shift + mode = fields.shift.oct + uid = fields.shift.oct + gid = fields.shift.oct + size = fields.shift.oct + mtime = fields.shift.oct + checksum = fields.shift.oct + typeflag = fields.shift + linkname = fields.shift + magic = fields.shift + version = fields.shift.oct + uname = fields.shift + gname = fields.shift + devmajor = fields.shift.oct + devminor = fields.shift.oct + prefix = fields.shift + + empty = (data == "\0" * 512) + + new(:name=>name, :mode=>mode, :uid=>uid, :gid=>gid, :size=>size, + :mtime=>mtime, :checksum=>checksum, :typeflag=>typeflag, + :magic=>magic, :version=>version, :uname=>uname, :gname=>gname, + :devmajor=>devmajor, :devminor=>devminor, :prefix=>prefix, + :empty => empty ) + end + + def initialize(vals) + unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode] + raise ArgumentError, ":name, :size, :prefix and :mode required" + end + vals[:uid] ||= 0 + vals[:gid] ||= 0 + vals[:mtime] ||= 0 + vals[:checksum] ||= "" + vals[:typeflag] ||= "0" + vals[:magic] ||= "ustar" + vals[:version] ||= "00" + vals[:uname] ||= "wheel" + vals[:gname] ||= "wheel" + vals[:devmajor] ||= 0 + vals[:devminor] ||= 0 + FIELDS.each {|x| instance_variable_set "@#{x.to_s}", vals[x]} + @empty = vals[:empty] + end + + def empty? + @empty + end + + def to_s + update_checksum + header(checksum) + end + + def update_checksum + h = header(" " * 8) + @checksum = oct(calculate_checksum(h), 6) + end + + private + def oct(num, len) + "%0#{len}o" % num + end + + def calculate_checksum(hdr) + hdr.unpack("C*").inject{|a,b| a+b} + end + + def header(chksum) + # struct tarfile_entry_posix { + # char name[100]; # ASCII + (Z unless filled) + # char mode[8]; # 0 padded, octal, null + # char uid[8]; # ditto + # char gid[8]; # ditto + # char size[12]; # 0 padded, octal, null + # char mtime[12]; # 0 padded, octal, null + # char checksum[8]; # 0 padded, octal, null, space + # char typeflag[1]; # file: "0" dir: "5" + # char linkname[100]; # ASCII + (Z unless filled) + # char magic[6]; # "ustar\0" + # char version[2]; # "00" + # char uname[32]; # ASCIIZ + # char gname[32]; # ASCIIZ + # char devmajor[8]; # 0 padded, octal, null + # char devminor[8]; # o padded, octal, null + # char prefix[155]; # ASCII + (Z unless filled) + # }; + arr = [name, oct(mode, 7), oct(uid, 7), oct(gid, 7), oct(size, 11), + oct(mtime, 11), chksum, " ", typeflag, linkname, magic, version, + uname, gname, oct(devmajor, 7), oct(devminor, 7), prefix] + str = arr.pack("a100a8a8a8a12a12" + # name, mode, uid, gid, size, mtime + "a7aaa100a6a2" + # chksum, typeflag, linkname, magic, version + "a32a32a8a8a155") # uname, gname, devmajor, devminor, prefix + str + "\0" * ((512 - str.size) % 512) + end + end + + class TarWriter + class FileOverflow < StandardError; end + class BlockNeeded < StandardError; end + + class BoundedStream + attr_reader :limit, :written + def initialize(io, limit) + @io = io + @limit = limit + @written = 0 + end + + def write(data) + if data.size + @written > @limit + raise FileOverflow, + "You tried to feed more data than fits in the file." + end + @io.write data + @written += data.size + data.size + end + end + + class RestrictedStream + def initialize(anIO) + @io = anIO + end + + def write(data) + @io.write data + end + end + + def self.new(anIO) + writer = super(anIO) + return writer unless block_given? + begin + yield writer + ensure + writer.close + end + nil + end + + def initialize(anIO) + @io = anIO + @closed = false + end + + def add_file_simple(name, mode, size) + raise BlockNeeded unless block_given? + raise ClosedIO if @closed + name, prefix = split_name(name) + header = TarHeader.new(:name => name, :mode => mode, + :size => size, :prefix => prefix).to_s + @io.write header + os = BoundedStream.new(@io, size) + yield os + #FIXME: what if an exception is raised in the block? + min_padding = size - os.written + @io.write("\0" * min_padding) + remainder = (512 - (size % 512)) % 512 + @io.write("\0" * remainder) + end + + def add_file(name, mode) + raise BlockNeeded unless block_given? + raise ClosedIO if @closed + raise NonSeekableIO unless @io.respond_to? :pos= + name, prefix = split_name(name) + init_pos = @io.pos + @io.write "\0" * 512 # placeholder for the header + yield RestrictedStream.new(@io) + #FIXME: what if an exception is raised in the block? + #FIXME: what if an exception is raised in the block? + size = @io.pos - init_pos - 512 + remainder = (512 - (size % 512)) % 512 + @io.write("\0" * remainder) + final_pos = @io.pos + @io.pos = init_pos + header = TarHeader.new(:name => name, :mode => mode, + :size => size, :prefix => prefix).to_s + @io.write header + @io.pos = final_pos + end + + def mkdir(name, mode) + raise ClosedIO if @closed + name, prefix = split_name(name) + header = TarHeader.new(:name => name, :mode => mode, :typeflag => "5", + :size => 0, :prefix => prefix).to_s + @io.write header + nil + end + + def flush + raise ClosedIO if @closed + @io.flush if @io.respond_to? :flush + end + + def close + #raise ClosedIO if @closed + return if @closed + @io.write "\0" * 1024 + @closed = true + end + + private + def split_name name + raise TooLongFileName if name.size > 256 + if name.size <= 100 + prefix = "" + else + parts = name.split(/\//) + newname = parts.pop + nxt = "" + loop do + nxt = parts.pop + break if newname.size + 1 + nxt.size > 100 + newname = nxt + "/" + newname + end + prefix = (parts + [nxt]).join "/" + name = newname + raise TooLongFileName if name.size > 100 || prefix.size > 155 + end + return name, prefix + end + end + + class TarReader + + include Gem::Package + + class UnexpectedEOF < StandardError; end + + module InvalidEntry + def read(len=nil); raise ClosedIO; end + def getc; raise ClosedIO; end + def rewind; raise ClosedIO; end + end + + class Entry + TarHeader::FIELDS.each{|x| attr_reader x} + + def initialize(header, anIO) + @io = anIO + @name = header.name + @mode = header.mode + @uid = header.uid + @gid = header.gid + @size = header.size + @mtime = header.mtime + @checksum = header.checksum + @typeflag = header.typeflag + @linkname = header.linkname + @magic = header.magic + @version = header.version + @uname = header.uname + @gname = header.gname + @devmajor = header.devmajor + @devminor = header.devminor + @prefix = header.prefix + @read = 0 + @orig_pos = @io.pos + end + + def read(len = nil) + return nil if @read >= @size + len ||= @size - @read + max_read = [len, @size - @read].min + ret = @io.read(max_read) + @read += ret.size + ret + end + + def getc + return nil if @read >= @size + ret = @io.getc + @read += 1 if ret + ret + end + + def is_directory? + @typeflag == "5" + end + + def is_file? + @typeflag == "0" + end + + def eof? + @read >= @size + end + + def pos + @read + end + + def rewind + raise NonSeekableIO unless @io.respond_to? :pos= + @io.pos = @orig_pos + @read = 0 + end + + alias_method :is_directory, :is_directory? + alias_method :is_file, :is_file + + def bytes_read + @read + end + + def full_name + if @prefix != "" + File.join(@prefix, @name) + else + @name + end + end + + def close + invalidate + end + + private + def invalidate + extend InvalidEntry + end + end + + def self.new(anIO) + reader = super(anIO) + return reader unless block_given? + begin + yield reader + ensure + reader.close + end + nil + end + + def initialize(anIO) + @io = anIO + @init_pos = anIO.pos + end + + def each(&block) + each_entry(&block) + end + + # do not call this during a #each or #each_entry iteration + def rewind + if @init_pos == 0 + raise NonSeekableIO unless @io.respond_to? :rewind + @io.rewind + else + raise NonSeekableIO unless @io.respond_to? :pos= + @io.pos = @init_pos + end + end + + def each_entry + loop do + return if @io.eof? + header = TarHeader.new_from_stream(@io) + return if header.empty? + entry = Entry.new header, @io + size = entry.size + yield entry + skip = (512 - (size % 512)) % 512 + if @io.respond_to? :seek + # avoid reading... + @io.seek(size - entry.bytes_read, IO::SEEK_CUR) + else + pending = size - entry.bytes_read + while pending > 0 + bread = @io.read([pending, 4096].min).size + raise UnexpectedEOF if @io.eof? + pending -= bread + end + end + @io.read(skip) # discard trailing zeros + # make sure nobody can use #read, #getc or #rewind anymore + entry.close + end + end + + def close + end + + end + + class TarInput + + include FSyncDir + include Enumerable + + attr_reader :metadata + + class << self; private :new end + + def initialize(io, security_policy = nil) + @io = io + @tarreader = TarReader.new(@io) + has_meta = false + data_sig, meta_sig, data_dgst, meta_dgst = nil, nil, nil, nil + dgst_algo = security_policy ? Gem::Security::OPT[:dgst_algo] : nil + + @tarreader.each do |entry| + case entry.full_name + when "metadata" + @metadata = load_gemspec(entry.read) + has_meta = true + break + when "metadata.gz" + begin + # if we have a security_policy, then pre-read the metadata file + # and calculate it's digest + sio = nil + if security_policy + Gem.ensure_ssl_available + sio = StringIO.new(entry.read) + meta_dgst = dgst_algo.digest(sio.string) + sio.rewind + end + + gzis = Zlib::GzipReader.new(sio || entry) + # YAML wants an instance of IO + @metadata = load_gemspec(gzis) + has_meta = true + ensure + gzis.close unless gzis.nil? + end + when 'metadata.gz.sig' + meta_sig = entry.read + when 'data.tar.gz.sig' + data_sig = entry.read + when 'data.tar.gz' + if security_policy + Gem.ensure_ssl_available + data_dgst = dgst_algo.digest(entry.read) + end + end + end + + if security_policy then + Gem.ensure_ssl_available + + # map trust policy from string to actual class (or a serialized YAML + # file, if that exists) + if String === security_policy then + if Gem::Security::Policy.key? security_policy then + # load one of the pre-defined security policies + security_policy = Gem::Security::Policy[security_policy] + elsif File.exist? security_policy then + # FIXME: this doesn't work yet + security_policy = YAML.load File.read(security_policy) + else + raise Gem::Exception, "Unknown trust policy '#{security_policy}'" + end + end + + if data_sig && data_dgst && meta_sig && meta_dgst then + # the user has a trust policy, and we have a signed gem + # file, so use the trust policy to verify the gem signature + + begin + security_policy.verify_gem(data_sig, data_dgst, @metadata.cert_chain) + rescue Exception => e + raise "Couldn't verify data signature: #{e}" + end + + begin + security_policy.verify_gem(meta_sig, meta_dgst, @metadata.cert_chain) + rescue Exception => e + raise "Couldn't verify metadata signature: #{e}" + end + elsif security_policy.only_signed + raise Gem::Exception, "Unsigned gem" + else + # FIXME: should display warning here (trust policy, but + # either unsigned or badly signed gem file) + end + end + + @tarreader.rewind + @fileops = Gem::FileOperations.new + raise FormatError, "No metadata found!" unless has_meta + end + + # Attempt to YAML-load a gemspec from the given _io_ parameter. Return + # nil if it fails. + def load_gemspec(io) + Gem::Specification.from_yaml(io) + rescue Gem::Exception + nil + end + + def self.open(filename, security_policy = nil, &block) + open_from_io(File.open(filename, "rb"), security_policy, &block) + end + + def self.open_from_io(io, security_policy = nil, &block) + raise "Want a block" unless block_given? + begin + is = new(io, security_policy) + yield is + ensure + is.close if is + end + end + + def each(&block) + @tarreader.each do |entry| + next unless entry.full_name == "data.tar.gz" + is = zipped_stream(entry) + begin + TarReader.new(is) do |inner| + inner.each(&block) + end + ensure + is.close if is + end + end + @tarreader.rewind + end + + # Return an IO stream for the zipped entry. + # + # NOTE: Originally this method used two approaches, Return a GZipReader + # directly, or read the GZipReader into a string and return a StringIO on + # the string. The string IO approach was used for versions of ZLib before + # 1.2.1 to avoid buffer errors on windows machines. Then we found that + # errors happened with 1.2.1 as well, so we changed the condition. Then + # we discovered errors occurred with versions as late as 1.2.3. At this + # point (after some benchmarking to show we weren't seriously crippling + # the unpacking speed) we threw our hands in the air and declared that + # this method would use the String IO approach on all platforms at all + # times. And that's the way it is. + def zipped_stream(entry) + # This is Jamis Buck's ZLib workaround. The original code is + # commented out while we evaluate this patch. + entry.read(10) # skip the gzip header + zis = Zlib::Inflate.new(-Zlib::MAX_WBITS) + is = StringIO.new(zis.inflate(entry.read)) + # zis = Zlib::GzipReader.new entry + # dis = zis.read + # is = StringIO.new(dis) + ensure + zis.finish if zis + end + + def extract_entry(destdir, entry, expected_md5sum = nil) + if entry.is_directory? + dest = File.join(destdir, entry.full_name) + if file_class.dir? dest + @fileops.chmod entry.mode, dest, :verbose=>false + else + @fileops.mkdir_p(dest, :mode => entry.mode, :verbose=>false) + end + fsync_dir dest + fsync_dir File.join(dest, "..") + return + end + # it's a file + md5 = Digest::MD5.new if expected_md5sum + destdir = File.join(destdir, File.dirname(entry.full_name)) + @fileops.mkdir_p(destdir, :mode => 0755, :verbose=>false) + destfile = File.join(destdir, File.basename(entry.full_name)) + @fileops.chmod(0600, destfile, :verbose=>false) rescue nil # Errno::ENOENT + file_class.open(destfile, "wb", entry.mode) do |os| + loop do + data = entry.read(4096) + break unless data + md5 << data if expected_md5sum + os.write(data) + end + os.fsync + end + @fileops.chmod(entry.mode, destfile, :verbose=>false) + fsync_dir File.dirname(destfile) + fsync_dir File.join(File.dirname(destfile), "..") + if expected_md5sum && expected_md5sum != md5.hexdigest + raise BadCheckSum + end + end + + def close + @io.close + @tarreader.close + end + + private + + def file_class + File + end + end + + class TarOutput + + class << self; private :new end + + def initialize(io) + @io = io + @external = TarWriter.new @io + end + + def external_handle + @external + end + + def self.open(filename, signer = nil, &block) + io = File.open(filename, "wb") + open_from_io(io, signer, &block) + nil + end + + def self.open_from_io(io, signer = nil, &block) + outputter = new(io) + metadata = nil + set_meta = lambda{|x| metadata = x} + raise "Want a block" unless block_given? + begin + data_sig, meta_sig = nil, nil + + outputter.external_handle.add_file("data.tar.gz", 0644) do |inner| + begin + sio = signer ? StringIO.new : nil + os = Zlib::GzipWriter.new(sio || inner) + + TarWriter.new(os) do |inner_tar_stream| + klass = class << inner_tar_stream; self end + klass.send(:define_method, :metadata=, &set_meta) + block.call inner_tar_stream + end + ensure + os.flush + os.finish + #os.close + + # if we have a signing key, then sign the data + # digest and return the signature + data_sig = nil + if signer + dgst_algo = Gem::Security::OPT[:dgst_algo] + dig = dgst_algo.digest(sio.string) + data_sig = signer.sign(dig) + inner.write(sio.string) + end + end + end + + # if we have a data signature, then write it to the gem too + if data_sig + sig_file = 'data.tar.gz.sig' + outputter.external_handle.add_file(sig_file, 0644) do |os| + os.write(data_sig) + end + end + + outputter.external_handle.add_file("metadata.gz", 0644) do |os| + begin + sio = signer ? StringIO.new : nil + gzos = Zlib::GzipWriter.new(sio || os) + gzos.write metadata + ensure + gzos.flush + gzos.finish + + # if we have a signing key, then sign the metadata + # digest and return the signature + if signer + dgst_algo = Gem::Security::OPT[:dgst_algo] + dig = dgst_algo.digest(sio.string) + meta_sig = signer.sign(dig) + os.write(sio.string) + end + end + end + + # if we have a metadata signature, then write to the gem as + # well + if meta_sig + sig_file = 'metadata.gz.sig' + outputter.external_handle.add_file(sig_file, 0644) do |os| + os.write(meta_sig) + end + end + + ensure + outputter.close + end + nil + end + + def close + @external.close + @io.close + end + + end + + #FIXME: refactor the following 2 methods + + def self.open(dest, mode = "r", signer = nil, &block) + raise "Block needed" unless block_given? + + case mode + when "r" + security_policy = signer + TarInput.open(dest, security_policy, &block) + when "w" + TarOutput.open(dest, signer, &block) + else + raise "Unknown Package open mode" + end + end + + def self.open_from_io(io, mode = "r", signer = nil, &block) + raise "Block needed" unless block_given? + + case mode + when "r" + security_policy = signer + TarInput.open_from_io(io, security_policy, &block) + when "w" + TarOutput.open_from_io(io, signer, &block) + else + raise "Unknown Package open mode" + end + end + + def self.pack(src, destname, signer = nil) + TarOutput.open(destname, signer) do |outp| + dir_class.chdir(src) do + outp.metadata = (file_class.read("RPA/metadata") rescue nil) + find_class.find('.') do |entry| + case + when file_class.file?(entry) + entry.sub!(%r{\./}, "") + next if entry =~ /\ARPA\// + stat = File.stat(entry) + outp.add_file_simple(entry, stat.mode, stat.size) do |os| + file_class.open(entry, "rb") do |f| + os.write(f.read(4096)) until f.eof? + end + end + when file_class.dir?(entry) + entry.sub!(%r{\./}, "") + next if entry == "RPA" + outp.mkdir(entry, file_class.stat(entry).mode) + else + raise "Don't know how to pack this yet!" + end + end + end + end + end + + class << self + def file_class + File + end + + def dir_class + Dir + end + + def find_class # HACK kill me + Find + end + end + +end + diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb new file mode 100644 index 0000000000..f72f3a7684 --- /dev/null +++ b/lib/rubygems/platform.rb @@ -0,0 +1,187 @@ +require 'rubygems' + +# Available list of platforms for targeting Gem installations. +# +class Gem::Platform + + @local = nil + + attr_accessor :cpu + + attr_accessor :os + + attr_accessor :version + + def self.local + arch = Config::CONFIG['arch'] + arch = "#{arch}_60" if arch =~ /mswin32$/ + @local ||= new(arch) + end + + def self.match(platform) + Gem.platforms.any? do |local_platform| + platform.nil? or local_platform == platform or + (local_platform != Gem::Platform::RUBY and local_platform =~ platform) + end + end + + def self.new(arch) # :nodoc: + case arch + when Gem::Platform::RUBY, nil then + Gem::Platform::RUBY + else + super + end + end + + def initialize(arch) + case arch + when Array then + @cpu, @os, @version = arch + when String then + arch = arch.split '-' + + if arch.length > 2 and arch.last !~ /\d/ then # reassemble x86-linux-gnu + extra = arch.pop + arch.last << "-#{extra}" + end + + cpu = arch.shift + + @cpu = case cpu + when /i\d86/ then 'x86' + else cpu + end + + if arch.length == 2 and arch.last =~ /^\d+$/ then # for command-line + @os, @version = arch + return + end + + os, = arch + @cpu, os = nil, cpu if os.nil? # legacy jruby + + @os, @version = case os + when /aix(\d+)/ then [ 'aix', $1 ] + when /cygwin/ then [ 'cygwin', nil ] + when /darwin(\d+)?/ then [ 'darwin', $1 ] + when /freebsd(\d+)/ then [ 'freebsd', $1 ] + when /hpux(\d+)/ then [ 'hpux', $1 ] + when /^java$/, /^jruby$/ then [ 'java', nil ] + when /^java([\d.]*)/ then [ 'java', $1 ] + when /linux/ then [ 'linux', $1 ] + when /mingw32/ then [ 'mingw32', nil ] + when /(mswin\d+)(\_(\d+))?/ then [ $1, $3 ] + when /netbsdelf/ then [ 'netbsdelf', nil ] + when /openbsd(\d+\.\d+)/ then [ 'openbsd', $1 ] + when /solaris(\d+\.\d+)/ then [ 'solaris', $1 ] + # test + when /^(\w+_platform)(\d+)/ then [ $1, $2 ] + else [ 'unknown', nil ] + end + when Gem::Platform then + @cpu = arch.cpu + @os = arch.os + @version = arch.version + else + raise ArgumentError, "invalid argument #{arch.inspect}" + end + end + + def inspect + "#<%s:0x%x @cpu=%p, @os=%p, @version=%p>" % [self.class, object_id, *to_a] + end + + def to_a + [@cpu, @os, @version] + end + + def to_s + to_a.compact.join '-' + end + + def ==(other) + self.class === other and + @cpu == other.cpu and @os == other.os and @version == other.version + end + + def ===(other) + return nil unless Gem::Platform === other + + # cpu + (@cpu == 'universal' or other.cpu == 'universal' or @cpu == other.cpu) and + + # os + @os == other.os and + + # version + (@version.nil? or other.version.nil? or @version == other.version) + end + + def =~(other) + case other + when Gem::Platform then # nop + when String then + # This data is from http://gems.rubyforge.org/gems/yaml on 19 Aug 2007 + other = case other + when /^i686-darwin(\d)/ then ['x86', 'darwin', $1] + when /^i\d86-linux/ then ['x86', 'linux', nil] + when 'java', 'jruby' then [nil, 'java', nil] + when /mswin32(\_(\d+))?/ then ['x86', 'mswin32', $2] + when 'powerpc-darwin' then ['powerpc', 'darwin', nil] + when /powerpc-darwin(\d)/ then ['powerpc', 'darwin', $1] + when /sparc-solaris2.8/ then ['sparc', 'solaris', '2.8'] + when /universal-darwin(\d)/ then ['universal', 'darwin', $1] + else other + end + + other = Gem::Platform.new other + else + return nil + end + + self === other + end + + ## + # A pure-ruby gem that may use Gem::Specification#extensions to build + # binary files. + + RUBY = 'ruby' + + ## + # A platform-specific gem that is built for the packaging ruby's platform. + # This will be replaced with Gem::Platform::local. + + CURRENT = 'current' + + ## + # A One Click Installer-compatible gem, built with VC6 for 32 bit Windows. + # + # CURRENT is preferred over this constant, avoid its use at all costs. + + MSWIN32 = new ['x86', 'mswin32', '60'] + + ## + # An x86 Linux-compatible gem + # + # CURRENT is preferred over this constant, avoid its use at all costs. + + X86_LINUX = new ['x86', 'linux', nil] + + ## + # A PowerPC Darwin-compatible gem + # + # CURRENT is preferred over this constant, avoid its use at all costs. + + PPC_DARWIN = new ['ppc', 'darwin', nil] + + # :stopdoc: + # Here lie legacy constants. These are deprecated. + WIN32 = 'mswin32' + LINUX_586 = 'i586-linux' + DARWIN = 'powerpc-darwin' + # :startdoc: + +end + diff --git a/lib/rubygems/remote_fetcher.rb b/lib/rubygems/remote_fetcher.rb new file mode 100644 index 0000000000..eac4ccaf01 --- /dev/null +++ b/lib/rubygems/remote_fetcher.rb @@ -0,0 +1,164 @@ +require 'net/http' +require 'uri' + +require 'rubygems' +require 'rubygems/gem_open_uri' + +## +# RemoteFetcher handles the details of fetching gems and gem information from +# a remote source. + +class Gem::RemoteFetcher + + class FetchError < Gem::Exception; end + + @fetcher = nil + + # Cached RemoteFetcher instance. + def self.fetcher + @fetcher ||= new Gem.configuration[:http_proxy] + end + + # Initialize a remote fetcher using the source URI and possible proxy + # information. + # + # +proxy+ + # * [String]: explicit specification of proxy; overrides any environment + # variable setting + # * nil: respect environment variables (HTTP_PROXY, HTTP_PROXY_USER, + # HTTP_PROXY_PASS) + # * :no_proxy: ignore environment variables and _don't_ use a proxy + def initialize(proxy) + @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 + end + + # Downloads +uri+. + def fetch_path(uri) + open_uri_or_path(uri) do |input| + input.read + end + rescue Timeout::Error + raise FetchError, "timed out fetching #{uri}" + rescue OpenURI::HTTPError, IOError, SocketError, SystemCallError => e + raise FetchError, "#{e.class}: #{e} reading #{uri}" + end + + # Returns the size of +uri+ in bytes. + def fetch_size(uri) + return File.size(get_file_uri_path(uri)) if file_uri? uri + + uri = URI.parse uri unless URI::Generic === uri + + raise ArgumentError, 'uri is not an HTTP URI' unless URI::HTTP === uri + + http = connect_to uri.host, uri.port + + request = Net::HTTP::Head.new uri.request_uri + + request.basic_auth unescape(uri.user), unescape(uri.password) unless + uri.user.nil? or uri.user.empty? + + resp = http.request request + + if resp.code !~ /^2/ then + raise Gem::RemoteSourceException, + "HTTP Response #{resp.code} fetching #{uri}" + end + + if resp['content-length'] then + return resp['content-length'].to_i + else + resp = http.get uri.request_uri + return resp.body.size + end + + rescue SocketError, SystemCallError, Timeout::Error => e + raise FetchError, "#{e.message} (#{e.class})" + end + + private + + def escape(str) + return unless str + URI.escape(str) + end + + def unescape(str) + return unless str + URI.unescape(str) + 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 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):/) ? uri : "http://#{uri}" + end + + # Connect to the source host/port, using a proxy if needed. + def connect_to(host, port) + if @proxy_uri + Net::HTTP::Proxy(@proxy_uri.host, @proxy_uri.port, unescape(@proxy_uri.user), unescape(@proxy_uri.password)).new(host, port) + else + Net::HTTP.new(host, port) + end + end + + # Read the data from the (source based) URI, but if it is a file:// URI, + # read from the filesystem instead. + def open_uri_or_path(uri, &block) + if file_uri?(uri) + open(get_file_uri_path(uri), &block) + else + connection_options = { + "User-Agent" => "RubyGems/#{Gem::RubyGemsVersion} #{Gem::Platform.local}" + } + + if @proxy_uri + http_proxy_url = "#{@proxy_uri.scheme}://#{@proxy_uri.host}:#{@proxy_uri.port}" + connection_options[:proxy_http_basic_authentication] = [http_proxy_url, unescape(@proxy_uri.user)||'', unescape(@proxy_uri.password)||''] + end + + uri = URI.parse uri unless URI::Generic === uri + unless uri.nil? || uri.user.nil? || uri.user.empty? then + connection_options[:http_basic_authentication] = + [unescape(uri.user), unescape(uri.password)] + end + + open(uri, connection_options, &block) + end + end + + # Checks if the provided string is a file:// URI. + def file_uri?(uri) + uri =~ %r{\Afile://} + end + + # Given a file:// URI, returns its local path. + def get_file_uri_path(uri) + uri.sub(%r{\Afile://}, '') + end + +end + diff --git a/lib/rubygems/remote_installer.rb b/lib/rubygems/remote_installer.rb new file mode 100644 index 0000000000..e33fd548f2 --- /dev/null +++ b/lib/rubygems/remote_installer.rb @@ -0,0 +1,195 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'fileutils' + +require 'rubygems' +require 'rubygems/installer' +require 'rubygems/source_info_cache' + +module Gem + + class RemoteInstaller + + include UserInteraction + + # options[:http_proxy]:: + # * [String]: explicit specification of proxy; overrides any + # environment variable setting + # * nil: respect environment variables (HTTP_PROXY, HTTP_PROXY_USER, HTTP_PROXY_PASS) + # * :no_proxy: ignore environment variables and _don't_ + # use a proxy + # + # * :cache_dir: override where downloaded gems are cached. + def initialize(options={}) + @options = options + @source_index_hash = nil + end + + # This method will install package_name onto the local system. + # + # gem_name:: + # [String] Name of the Gem to install + # + # version_requirement:: + # [default = ">= 0"] Gem version requirement to install + # + # Returns:: + # an array of Gem::Specification objects, one for each gem installed. + # + def install(gem_name, version_requirement = Gem::Requirement.default, + force = false, install_dir = Gem.dir) + unless version_requirement.respond_to?(:satisfied_by?) + version_requirement = Gem::Requirement.new [version_requirement] + end + installed_gems = [] + begin + spec, source = find_gem_to_install(gem_name, version_requirement) + dependencies = find_dependencies_not_installed(spec.dependencies) + + installed_gems << install_dependencies(dependencies, force, install_dir) + + cache_dir = @options[:cache_dir] || File.join(install_dir, "cache") + destination_file = File.join(cache_dir, spec.full_name + ".gem") + + download_gem(destination_file, source, spec) + + installer = new_installer(destination_file) + installed_gems.unshift installer.install(force, install_dir) + rescue RemoteInstallationSkipped => e + alert_error e.message + end + installed_gems.flatten + end + + # Return a hash mapping the available source names to the source + # index of that source. + def source_index_hash + return @source_index_hash if @source_index_hash + @source_index_hash = {} + Gem::SourceInfoCache.cache_data.each do |source_uri, sic_entry| + @source_index_hash[source_uri] = sic_entry.source_index + end + @source_index_hash + end + + # Finds the Gem::Specification objects and the corresponding source URI + # for gems matching +gem_name+ and +version_requirement+ + def specs_n_sources_matching(gem_name, version_requirement) + specs_n_sources = [] + + source_index_hash.each do |source_uri, source_index| + specs = source_index.search(/^#{Regexp.escape gem_name}$/i, + version_requirement) + # TODO move to SourceIndex#search? + ruby_version = Gem::Version.new RUBY_VERSION + specs = specs.select do |spec| + spec.required_ruby_version.nil? or + spec.required_ruby_version.satisfied_by? ruby_version + end + specs.each { |spec| specs_n_sources << [spec, source_uri] } + end + + if specs_n_sources.empty? then + raise GemNotFoundException, "Could not find #{gem_name} (#{version_requirement}) in any repository" + end + + specs_n_sources = specs_n_sources.sort_by { |gs,| gs.version }.reverse + + specs_n_sources + end + + # Find a gem to be installed by interacting with the user. + def find_gem_to_install(gem_name, version_requirement) + specs_n_sources = specs_n_sources_matching gem_name, version_requirement + + top_3_versions = specs_n_sources.map{|gs| gs.first.version}.uniq[0..3] + specs_n_sources.reject!{|gs| !top_3_versions.include?(gs.first.version)} + + binary_gems = specs_n_sources.reject { |item| + item[0].platform.nil? || item[0].platform==Platform::RUBY + } + + # only non-binary gems...return latest + return specs_n_sources.first if binary_gems.empty? + + list = specs_n_sources.collect { |spec, source_uri| + "#{spec.name} #{spec.version} (#{spec.platform})" + } + + list << "Skip this gem" + list << "Cancel installation" + + string, index = choose_from_list( + "Select which gem to install for your platform (#{RUBY_PLATFORM})", + list) + + if index.nil? or index == (list.size - 1) then + raise RemoteInstallationCancelled, "Installation of #{gem_name} cancelled." + end + + if index == (list.size - 2) then + raise RemoteInstallationSkipped, "Installation of #{gem_name} skipped." + end + + specs_n_sources[index] + end + + def find_dependencies_not_installed(dependencies) + to_install = [] + dependencies.each do |dependency| + srcindex = Gem::SourceIndex.from_installed_gems + matches = srcindex.find_name(dependency.name, dependency.requirement_list) + to_install.push dependency if matches.empty? + end + to_install + end + + # Install all the given dependencies. Returns an array of + # Gem::Specification objects, one for each dependency installed. + # + # TODO: For now, we recursively install, but this is not the right + # way to do things (e.g. if a package fails to download, we + # shouldn't install anything). + def install_dependencies(dependencies, force, install_dir) + return if @options[:ignore_dependencies] + installed_gems = [] + dependencies.each do |dep| + if @options[:include_dependencies] || + ask_yes_no("Install required dependency #{dep.name}?", true) + remote_installer = RemoteInstaller.new @options + installed_gems << remote_installer.install(dep.name, + dep.version_requirements, + force, install_dir) + elsif force then + # ignore + else + raise DependencyError, "Required dependency #{dep.name} not installed" + end + end + installed_gems + end + + def download_gem(destination_file, source, spec) + return if File.exist? destination_file + uri = source + "/gems/#{spec.full_name}.gem" + response = Gem::RemoteFetcher.fetcher.fetch_path uri + write_gem_to_file response, destination_file + end + + def write_gem_to_file(body, destination_file) + FileUtils.mkdir_p(File.dirname(destination_file)) unless File.exist?(destination_file) + File.open(destination_file, 'wb') do |out| + out.write(body) + end + end + + def new_installer(gem) + return Installer.new(gem, @options) + end + end + +end diff --git a/lib/rubygems/requirement.rb b/lib/rubygems/requirement.rb new file mode 100644 index 0000000000..4dfba4fa61 --- /dev/null +++ b/lib/rubygems/requirement.rb @@ -0,0 +1,157 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems/version' + +## +# Requirement version includes a prefaced comparator in addition +# to a version number. +# +# A Requirement object can actually contain multiple, er, +# requirements, as in (> 1.2, < 2.0). +class Gem::Requirement + + include Comparable + + OPS = { + "=" => lambda { |v, r| v == r }, + "!=" => lambda { |v, r| v != r }, + ">" => lambda { |v, r| v > r }, + "<" => lambda { |v, r| v < r }, + ">=" => lambda { |v, r| v >= r }, + "<=" => lambda { |v, r| v <= r }, + "~>" => lambda { |v, r| v >= r && v < r.bump } + } + + OP_RE = /#{OPS.keys.map{ |k| Regexp.quote k }.join '|'}/o + + ## + # Factory method to create a Gem::Requirement object. Input may be a + # Version, a String, or nil. Intended to simplify client code. + # + # If the input is "weird", the default version requirement is returned. + # + def self.create(input) + case input + when Gem::Requirement then + input + when Gem::Version, Array then + new input + else + if input.respond_to? :to_str then + self.new [input.to_str] + else + self.default + end + end + end + + ## + # A default "version requirement" can surely _only_ be '>= 0'. + #-- + # This comment once said: + # + # "A default "version requirement" can surely _only_ be '> 0'." + def self.default + self.new ['>= 0'] + end + + ## + # Constructs a Requirement from +requirements+ which can be a String, a + # Gem::Version, or an Array of those. See parse for details on the + # formatting of requirement strings. + def initialize(requirements) + @requirements = case requirements + when Array then + requirements.map do |requirement| + parse(requirement) + end + else + [parse(requirements)] + end + @version = nil # Avoid warnings. + end + + # Marshal raw requirements, rather than the full object + def marshal_dump + [@requirements] + end + + # Load custom marshal format + def marshal_load(array) + @requirements = array[0] + @version = nil + end + + def to_s # :nodoc: + as_list.join(", ") + end + + def as_list + normalize + @requirements.collect { |req| + "#{req[0]} #{req[1]}" + } + end + + def normalize + return if not defined? @version or @version.nil? + @requirements = [parse(@version)] + @nums = nil + @version = nil + @op = nil + end + + ## + # Is the requirement satifised by +version+. + # + # version:: [Gem::Version] the version to compare against + # return:: [Boolean] true if this requirement is satisfied by + # the version, otherwise false + # + def satisfied_by?(version) + normalize + @requirements.all? { |op, rv| satisfy?(op, version, rv) } + end + + ## + # Is "version op required_version" satisfied? + # + def satisfy?(op, version, required_version) + OPS[op].call(version, required_version) + end + + ## + # Parse the version requirement obj returning the operator and version. + # + # The requirement can be a String or a Gem::Version. A String can be an + # operator (<, <=, =, =>, >, !=, ~>), a version number, or both, operator + # first. + def parse(obj) + case obj + when /^\s*(#{OP_RE})\s*([0-9.]+)\s*$/o then + [$1, Gem::Version.new($2)] + when /^\s*([0-9.]+)\s*$/ then + ['=', Gem::Version.new($1)] + when /^\s*(#{OP_RE})\s*$/o then + [$1, Gem::Version.new('0')] + when Gem::Version then + ['=', obj] + else + fail ArgumentError, "Illformed requirement [#{obj.inspect}]" + end + end + + def <=>(other) + to_s <=> other.to_s + end + + def hash # :nodoc: + to_s.hash + end + +end + diff --git a/lib/rubygems/rubygems_version.rb b/lib/rubygems/rubygems_version.rb new file mode 100644 index 0000000000..e01588ef2d --- /dev/null +++ b/lib/rubygems/rubygems_version.rb @@ -0,0 +1,6 @@ +# DO NOT EDIT +# This file is auto-generated by build scripts. +# See: rake update_version +module Gem + RubyGemsVersion = '0.9.4.6' +end diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb new file mode 100644 index 0000000000..6f6586e9cf --- /dev/null +++ b/lib/rubygems/security.rb @@ -0,0 +1,785 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems/gem_openssl' + +# = Signed Gems README +# +# == Table of Contents +# * Overview +# * Walkthrough +# * Command-Line Options +# * OpenSSL Reference +# * Bugs/TODO +# * About the Author +# +# == Overview +# +# Gem::Security implements cryptographic signatures in RubyGems. The section +# below is a step-by-step guide to using signed gems and generating your own. +# +# == Walkthrough +# +# In order to start signing your gems, you'll need to build a private key and +# a self-signed certificate. Here's how: +# +# # build a private key and certificate for gemmaster@example.com +# $ gem cert --build gemmaster@example.com +# +# This could take anywhere from 5 seconds to 10 minutes, depending on the +# speed of your computer (public key algorithms aren't exactly the speediest +# crypto algorithms in the world). When it's finished, you'll see the files +# "gem-private_key.pem" and "gem-public_cert.pem" in the current directory. +# +# First things first: take the "gem-private_key.pem" file and move it +# somewhere private, preferably a directory only you have access to, a floppy +# (yuck!), a CD-ROM, or something comparably secure. Keep your private key +# hidden; if it's compromised, someone can sign packages as you (note: PKI has +# ways of mitigating the risk of stolen keys; more on that later). +# +# Now, let's sign an existing gem. I'll be using my Imlib2-Ruby bindings, but +# you can use whatever gem you'd like. Open up your existing gemspec file and +# add the following lines: +# +# # signing key and certificate chain +# s.signing_key = '/mnt/floppy/gem-private_key.pem' +# s.cert_chain = ['gem-public_cert.pem'] +# +# (Be sure to replace "/mnt/floppy" with the ultra-secret path to your private +# key). +# +# After that, go ahead and build your gem as usual. Congratulations, you've +# just built your first signed gem! If you peek inside your gem file, you'll +# see a couple of new files have been added: +# +# $ tar tf tar tf Imlib2-Ruby-0.5.0.gem +# data.tar.gz +# data.tar.gz.sig +# metadata.gz +# metadata.gz.sig +# +# Now let's verify the signature. Go ahead and install the gem, but add the +# following options: "-P HighSecurity", like this: +# +# # install the gem with using the security policy "HighSecurity" +# $ sudo gem install Imlib2-Ruby-0.5.0.gem -P HighSecurity +# +# The -P option sets your security policy -- we'll talk about that in just a +# minute. Eh, what's this? +# +# Attempting local installation of 'Imlib2-Ruby-0.5.0.gem' +# ERROR: Error installing gem Imlib2-Ruby-0.5.0.gem[.gem]: Couldn't +# verify data signature: Untrusted Signing Chain Root: cert = +# '/CN=gemmaster/DC=example/DC=com', error = 'path +# "/root/.rubygems/trust/cert-15dbb43a6edf6a70a85d4e784e2e45312cff7030.pem" +# does not exist' +# +# The culprit here is the security policy. RubyGems has several different +# security policies. Let's take a short break and go over the security +# policies. Here's a list of the available security policies, and a brief +# description of each one: +# +# * NoSecurity - Well, no security at all. Signed packages are treated like +# unsigned packages. +# * LowSecurity - Pretty much no security. If a package is signed then +# RubyGems will make sure the signature matches the signing +# certificate, and that the signing certificate hasn't expired, but +# that's it. A malicious user could easily circumvent this kind of +# security. +# * MediumSecurity - Better than LowSecurity and NoSecurity, but still +# fallible. Package contents are verified against the signing +# certificate, and the signing certificate is checked for validity, +# and checked against the rest of the certificate chain (if you don't +# know what a certificate chain is, stay tuned, we'll get to that). +# The biggest improvement over LowSecurity is that MediumSecurity +# won't install packages that are signed by untrusted sources. +# Unfortunately, MediumSecurity still isn't totally secure -- a +# malicious user can still unpack the gem, strip the signatures, and +# distribute the gem unsigned. +# * HighSecurity - Here's the bugger that got us into this mess. +# The HighSecurity policy is identical to the MediumSecurity policy, +# except that it does not allow unsigned gems. A malicious user +# doesn't have a whole lot of options here; he can't modify the +# package contents without invalidating the signature, and he can't +# modify or remove signature or the signing certificate chain, or +# RubyGems will simply refuse to install the package. Oh well, maybe +# he'll have better luck causing problems for CPAN users instead :). +# +# So, the reason RubyGems refused to install our shiny new signed gem was +# because it was from an untrusted source. Well, my code is infallible +# (hah!), so I'm going to add myself as a trusted source. +# +# Here's how: +# +# # add trusted certificate +# gem cert --add gem-public_cert.pem +# +# I've added my public certificate as a trusted source. Now I can install +# packages signed my private key without any hassle. Let's try the install +# command above again: +# +# # install the gem with using the HighSecurity policy (and this time +# # without any shenanigans) +# $ sudo gem install Imlib2-Ruby-0.5.0.gem -P HighSecurity +# +# This time RubyGems should accept your signed package and begin installing. +# While you're waiting for RubyGems to work it's magic, have a look at some of +# the other security commands: +# +# Usage: gem cert [options] +# +# Options: +# -a, --add CERT Add a trusted certificate. +# -l, --list List trusted certificates. +# -r, --remove STRING Remove trusted certificates containing STRING. +# -b, --build EMAIL_ADDR Build private key and self-signed certificate +# for EMAIL_ADDR. +# -C, --certificate CERT Certificate for --sign command. +# -K, --private-key KEY Private key for --sign command. +# -s, --sign NEWCERT Sign a certificate with my key and certificate. +# +# (By the way, you can pull up this list any time you'd like by typing "gem +# cert --help") +# +# Hmm. We've already covered the "--build" option, and the "--add", "--list", +# and "--remove" commands seem fairly straightforward; they allow you to add, +# list, and remove the certificates in your trusted certificate list. But +# what's with this "--sign" option? +# +# To answer that question, let's take a look at "certificate chains", a +# concept I mentioned earlier. There are a couple of problems with +# self-signed certificates: first of all, self-signed certificates don't offer +# a whole lot of security. Sure, the certificate says Yukihiro Matsumoto, but +# how do I know it was actually generated and signed by matz himself unless he +# gave me the certificate in person? +# +# The second problem is scalability. Sure, if there are 50 gem authors, then +# I have 50 trusted certificates, no problem. What if there are 500 gem +# authors? 1000? Having to constantly add new trusted certificates is a +# pain, and it actually makes the trust system less secure by encouraging +# RubyGems users to blindly trust new certificates. +# +# Here's where certificate chains come in. A certificate chain establishes an +# arbitrarily long chain of trust between an issuing certificate and a child +# certificate. So instead of trusting certificates on a per-developer basis, +# we use the PKI concept of certificate chains to build a logical hierarchy of +# trust. Here's a hypothetical example of a trust hierarchy based (roughly) +# on geography: +# +# +# -------------------------- +# | rubygems@rubyforge.org | +# -------------------------- +# | +# ----------------------------------- +# | | +# ---------------------------- ----------------------------- +# | seattle.rb@zenspider.com | | dcrubyists@richkilmer.com | +# ---------------------------- ----------------------------- +# | | | | +# --------------- ---------------- ----------- -------------- +# | alf@seattle | | bob@portland | | pabs@dc | | tomcope@dc | +# --------------- ---------------- ----------- -------------- +# +# +# Now, rather than having 4 trusted certificates (one for alf@seattle, +# bob@portland, pabs@dc, and tomecope@dc), a user could actually get by with 1 +# certificate: the "rubygems@rubyforge.org" certificate. Here's how it works: +# +# I install "Alf2000-Ruby-0.1.0.gem", a package signed by "alf@seattle". I've +# never heard of "alf@seattle", but his certificate has a valid signature from +# the "seattle.rb@zenspider.com" certificate, which in turn has a valid +# signature from the "rubygems@rubyforge.org" certificate. Voila! At this +# point, it's much more reasonable for me to trust a package signed by +# "alf@seattle", because I can establish a chain to "rubygems@rubyforge.org", +# which I do trust. +# +# And the "--sign" option allows all this to happen. A developer creates +# their build certificate with the "--build" option, then has their +# certificate signed by taking it with them to their next regional Ruby meetup +# (in our hypothetical example), and it's signed there by the person holding +# the regional RubyGems signing certificate, which is signed at the next +# RubyConf by the holder of the top-level RubyGems certificate. At each point +# the issuer runs the same command: +# +# # sign a certificate with the specified key and certificate +# # (note that this modifies client_cert.pem!) +# $ gem cert -K /mnt/floppy/issuer-priv_key.pem -C issuer-pub_cert.pem +# --sign client_cert.pem +# +# Then the holder of issued certificate (in this case, our buddy +# "alf@seattle"), can start using this signed certificate to sign RubyGems. +# By the way, in order to let everyone else know about his new fancy signed +# certificate, "alf@seattle" would change his gemspec file to look like this: +# +# # signing key (still kept in an undisclosed location!) +# s.signing_key = '/mnt/floppy/alf-private_key.pem' +# +# # certificate chain (includes the issuer certificate now too) +# s.cert_chain = ['/home/alf/doc/seattlerb-public_cert.pem', +# '/home/alf/doc/alf_at_seattle-public_cert.pem'] +# +# Obviously, this RubyGems trust infrastructure doesn't exist yet. Also, in +# the "real world" issuers actually generate the child certificate from a +# certificate request, rather than sign an existing certificate. And our +# hypothetical infrastructure is missing a certificate revocation system. +# These are that can be fixed in the future... +# +# I'm sure your new signed gem has finished installing by now (unless you're +# installing rails and all it's dependencies, that is ;D). At this point you +# should know how to do all of these new and interesting things: +# +# * build a gem signing key and certificate +# * modify your existing gems to support signing +# * adjust your security policy +# * modify your trusted certificate list +# * sign a certificate +# +# If you've got any questions, feel free to contact me at the email address +# below. The next couple of sections +# +# +# == Command-Line Options +# +# Here's a brief summary of the certificate-related command line options: +# +# gem install +# -P, --trust-policy POLICY Specify gem trust policy. +# +# gem cert +# -a, --add CERT Add a trusted certificate. +# -l, --list List trusted certificates. +# -r, --remove STRING Remove trusted certificates containing +# STRING. +# -b, --build EMAIL_ADDR Build private key and self-signed +# certificate for EMAIL_ADDR. +# -C, --certificate CERT Certificate for --sign command. +# -K, --private-key KEY Private key for --sign command. +# -s, --sign NEWCERT Sign a certificate with my key and +# certificate. +# +# A more detailed description of each options is available in the walkthrough +# above. +# +# +# == OpenSSL Reference +# +# The .pem files generated by --build and --sign are just basic OpenSSL PEM +# files. Here's a couple of useful commands for manipulating them: +# +# # convert a PEM format X509 certificate into DER format: +# # (note: Windows .cer files are X509 certificates in DER format) +# $ openssl x509 -in input.pem -outform der -out output.der +# +# # print out the certificate in a human-readable format: +# $ openssl x509 -in input.pem -noout -text +# +# And you can do the same thing with the private key file as well: +# +# # convert a PEM format RSA key into DER format: +# $ openssl rsa -in input_key.pem -outform der -out output_key.der +# +# # print out the key in a human readable format: +# $ openssl rsa -in input_key.pem -noout -text +# +# == Bugs/TODO +# +# * There's no way to define a system-wide trust list. +# * custom security policies (from a YAML file, etc) +# * Simple method to generate a signed certificate request +# * Support for OCSP, SCVP, CRLs, or some other form of cert +# status check (list is in order of preference) +# * Support for encrypted private keys +# * Some sort of semi-formal trust hierarchy (see long-winded explanation +# above) +# * Path discovery (for gem certificate chains that don't have a self-signed +# root) -- by the way, since we don't have this, THE ROOT OF THE CERTIFICATE +# CHAIN MUST BE SELF SIGNED if Policy#verify_root is true (and it is for the +# MediumSecurity and HighSecurity policies) +# * Better explanation of X509 naming (ie, we don't have to use email +# addresses) +# * Possible alternate signing mechanisms (eg, via PGP). this could be done +# pretty easily by adding a :signing_type attribute to the gemspec, then add +# the necessary support in other places +# * Honor AIA field (see note about OCSP above) +# * Maybe honor restriction extensions? +# * Might be better to store the certificate chain as a PKCS#7 or PKCS#12 +# file, instead of an array embedded in the metadata. ideas? +# * Possibly embed signature and key algorithms into metadata (right now +# they're assumed to be the same as what's set in Gem::Security::OPT) +# +# == About the Author +# +# Paul Duncan +# http://pablotron.org/ + +module Gem::Security + + class Exception < Exception; end + + # + # default options for most of the methods below + # + OPT = { + # private key options + :key_algo => Gem::SSL::PKEY_RSA, + :key_size => 2048, + + # public cert options + :cert_age => 365 * 24 * 3600, # 1 year + :dgst_algo => Gem::SSL::DIGEST_SHA1, + + # x509 certificate extensions + :cert_exts => { + 'basicConstraints' => 'CA:FALSE', + 'subjectKeyIdentifier' => 'hash', + 'keyUsage' => 'keyEncipherment,dataEncipherment,digitalSignature', + }, + + # save the key and cert to a file in build_self_signed_cert()? + :save_key => true, + :save_cert => true, + + # if you define either of these, then they'll be used instead of + # the output_fmt macro below + :save_key_path => nil, + :save_cert_path => nil, + + # output name format for self-signed certs + :output_fmt => 'gem-%s.pem', + :munge_re => Regexp.new(/[^a-z0-9_.-]+/), + + # output directory for trusted certificate checksums + :trust_dir => File::join(Gem.user_home, '.gem', 'trust'), + + # default permissions for trust directory and certs + :perms => { + :trust_dir => 0700, + :trusted_cert => 0600, + :signing_cert => 0600, + :signing_key => 0600, + }, + } + + # + # A Gem::Security::Policy object encapsulates the settings for verifying + # signed gem files. This is the base class. You can either declare an + # instance of this or use one of the preset security policies below. + # + class Policy + attr_accessor :verify_data, :verify_signer, :verify_chain, + :verify_root, :only_trusted, :only_signed + + # + # Create a new Gem::Security::Policy object with the given mode and + # options. + # + def initialize(policy = {}, opt = {}) + # set options + @opt = Gem::Security::OPT.merge(opt) + + # build policy + policy.each_pair do |key, val| + case key + when :verify_data then @verify_data = val + when :verify_signer then @verify_signer = val + when :verify_chain then @verify_chain = val + when :verify_root then @verify_root = val + when :only_trusted then @only_trusted = val + when :only_signed then @only_signed = val + end + end + end + + # + # Get the path to the file for this cert. + # + def self.trusted_cert_path(cert, opt = {}) + opt = Gem::Security::OPT.merge(opt) + + # get digest algorithm, calculate checksum of root.subject + algo = opt[:dgst_algo] + dgst = algo.hexdigest(cert.subject.to_s) + + # build path to trusted cert file + name = "cert-#{dgst}.pem" + + # join and return path components + File::join(opt[:trust_dir], name) + end + + # + # Verify that the gem data with the given signature and signing chain + # matched this security policy at the specified time. + # + def verify_gem(signature, data, chain, time = Time.now) + Gem.ensure_ssl_available + cert_class = OpenSSL::X509::Certificate + exc = Gem::Security::Exception + chain ||= [] + + chain = chain.map{ |str| cert_class.new(str) } + signer, ch_len = chain[-1], chain.size + + # make sure signature is valid + if @verify_data + # get digest algorithm (TODO: this should be configurable) + dgst = @opt[:dgst_algo] + + # verify the data signature (this is the most important part, so don't + # screw it up :D) + v = signer.public_key.verify(dgst.new, signature, data) + raise exc, "Invalid Gem Signature" unless v + + # make sure the signer is valid + if @verify_signer + # make sure the signing cert is valid right now + v = signer.check_validity(nil, time) + raise exc, "Invalid Signature: #{v[:desc]}" unless v[:is_valid] + end + end + + # make sure the certificate chain is valid + if @verify_chain + # iterate down over the chain and verify each certificate against it's + # issuer + (ch_len - 1).downto(1) do |i| + issuer, cert = chain[i - 1, 2] + v = cert.check_validity(issuer, time) + raise exc, "%s: cert = '%s', error = '%s'" % [ + 'Invalid Signing Chain', cert.subject, v[:desc] + ] unless v[:is_valid] + end + + # verify root of chain + if @verify_root + # make sure root is self-signed + root = chain[0] + raise exc, "%s: %s (subject = '%s', issuer = '%s')" % [ + 'Invalid Signing Chain Root', + 'Subject does not match Issuer for Gem Signing Chain', + root.subject.to_s, + root.issuer.to_s, + ] unless root.issuer.to_s == root.subject.to_s + + # make sure root is valid + v = root.check_validity(root, time) + raise exc, "%s: cert = '%s', error = '%s'" % [ + 'Invalid Signing Chain Root', root.subject, v[:desc] + ] unless v[:is_valid] + + # verify that the chain root is trusted + if @only_trusted + # get digest algorithm, calculate checksum of root.subject + algo = @opt[:dgst_algo] + path = Gem::Security::Policy.trusted_cert_path(root, @opt) + + # check to make sure trusted path exists + raise exc, "%s: cert = '%s', error = '%s'" % [ + 'Untrusted Signing Chain Root', + root.subject.to_s, + "path \"#{path}\" does not exist", + ] unless File.exist?(path) + + # load calculate digest from saved cert file + save_cert = OpenSSL::X509::Certificate.new(File.read(path)) + save_dgst = algo.digest(save_cert.public_key.to_s) + + # create digest of public key + pkey_str = root.public_key.to_s + cert_dgst = algo.digest(pkey_str) + + # now compare the two digests, raise exception + # if they don't match + raise exc, "%s: %s (saved = '%s', root = '%s')" % [ + 'Invalid Signing Chain Root', + "Saved checksum doesn't match root checksum", + save_dgst, cert_dgst, + ] unless save_dgst == cert_dgst + end + end + + # return the signing chain + chain.map { |cert| cert.subject } + end + end + end + + # + # No security policy: all package signature checks are disabled. + # + NoSecurity = Policy.new( + :verify_data => false, + :verify_signer => false, + :verify_chain => false, + :verify_root => false, + :only_trusted => false, + :only_signed => false + ) + + # + # AlmostNo security policy: only verify that the signing certificate is the + # one that actually signed the data. Make no attempt to verify the signing + # certificate chain. + # + # This policy is basically useless. better than nothing, but can still be + # easily spoofed, and is not recommended. + # + AlmostNoSecurity = Policy.new( + :verify_data => true, + :verify_signer => false, + :verify_chain => false, + :verify_root => false, + :only_trusted => false, + :only_signed => false + ) + + # + # Low security policy: only verify that the signing certificate is actually + # the gem signer, and that the signing certificate is valid. + # + # This policy is better than nothing, but can still be easily spoofed, and + # is not recommended. + # + LowSecurity = Policy.new( + :verify_data => true, + :verify_signer => true, + :verify_chain => false, + :verify_root => false, + :only_trusted => false, + :only_signed => false + ) + + # + # Medium security policy: verify the signing certificate, verify the signing + # certificate chain all the way to the root certificate, and only trust root + # certificates that we have explicity allowed trust for. + # + # This security policy is reasonable, but it allows unsigned packages, so a + # malicious person could simply delete the package signature and pass the + # gem off as unsigned. + # + MediumSecurity = Policy.new( + :verify_data => true, + :verify_signer => true, + :verify_chain => true, + :verify_root => true, + :only_trusted => true, + :only_signed => false + ) + + # + # High security policy: only allow signed gems to be installed, verify the + # signing certificate, verify the signing certificate chain all the way to + # the root certificate, and only trust root certificates that we have + # explicity allowed trust for. + # + # This security policy is significantly more difficult to bypass, and offers + # a reasonable guarantee that the contents of the gem have not been altered. + # + HighSecurity = Policy.new( + :verify_data => true, + :verify_signer => true, + :verify_chain => true, + :verify_root => true, + :only_trusted => true, + :only_signed => true + ) + + # + # Hash of configured security policies + # + Policies = { + 'NoSecurity' => NoSecurity, + 'AlmostNoSecurity' => AlmostNoSecurity, + 'LowSecurity' => LowSecurity, + 'MediumSecurity' => MediumSecurity, + 'HighSecurity' => HighSecurity, + } + + # + # Sign the cert cert with @signing_key and @signing_cert, using the digest + # algorithm opt[:dgst_algo]. Returns the newly signed certificate. + # + def self.sign_cert(cert, signing_key, signing_cert, opt = {}) + opt = OPT.merge(opt) + + # set up issuer information + cert.issuer = signing_cert.subject + cert.sign(signing_key, opt[:dgst_algo].new) + + cert + end + + # + # Make sure the trust directory exists. If it does exist, make sure it's + # actually a directory. If not, then create it with the appropriate + # permissions. + # + def self.verify_trust_dir(path, perms) + # if the directory exists, then make sure it is in fact a directory. if + # it doesn't exist, then create it with the appropriate permissions + if File.exist?(path) + # verify that the trust directory is actually a directory + unless File.directory?(path) + err = "trust directory #{path} isn't a directory" + raise Gem::Security::Exception, err + end + else + # trust directory doesn't exist, so create it with permissions + FileUtils.mkdir_p(path) + FileUtils.chmod(perms, path) + end + end + + # + # Build a certificate from the given DN and private key. + # + def self.build_cert(name, key, opt = {}) + Gem.ensure_ssl_available + opt = OPT.merge(opt) + + # create new cert + ret = OpenSSL::X509::Certificate.new + + # populate cert attributes + ret.version = 2 + ret.serial = 0 + ret.public_key = key.public_key + ret.not_before = Time.now + ret.not_after = Time.now + opt[:cert_age] + ret.subject = name + + # add certificate extensions + ef = OpenSSL::X509::ExtensionFactory.new(nil, ret) + ret.extensions = opt[:cert_exts].map { |k, v| ef.create_extension(k, v) } + + # sign cert + i_key, i_cert = opt[:issuer_key] || key, opt[:issuer_cert] || ret + ret = sign_cert(ret, i_key, i_cert, opt) + + # return cert + ret + end + + # + # Build a self-signed certificate for the given email address. + # + def self.build_self_signed_cert(email_addr, opt = {}) + Gem.ensure_ssl_available + opt = OPT.merge(opt) + path = { :key => nil, :cert => nil } + + # split email address up + cn, dcs = email_addr.split('@') + dcs = dcs.split('.') + + # munge email CN and DCs + cn = cn.gsub(opt[:munge_re], '_') + dcs = dcs.map { |dc| dc.gsub(opt[:munge_re], '_') } + + # create DN + name = "CN=#{cn}/" << dcs.map { |dc| "DC=#{dc}" }.join('/') + name = OpenSSL::X509::Name::parse(name) + + # build private key + key = opt[:key_algo].new(opt[:key_size]) + + # method name pretty much says it all :) + verify_trust_dir(opt[:trust_dir], opt[:perms][:trust_dir]) + + # if we're saving the key, then write it out + if opt[:save_key] + path[:key] = opt[:save_key_path] || (opt[:output_fmt] % 'private_key') + File.open(path[:key], 'wb') do |file| + file.chmod(opt[:perms][:signing_key]) + file.write(key.to_pem) + end + end + + # build self-signed public cert from key + cert = build_cert(name, key, opt) + + # if we're saving the cert, then write it out + if opt[:save_cert] + path[:cert] = opt[:save_cert_path] || (opt[:output_fmt] % 'public_cert') + File.open(path[:cert], 'wb') do |file| + file.chmod(opt[:perms][:signing_cert]) + file.write(cert.to_pem) + end + end + + # return key, cert, and paths (if applicable) + { :key => key, :cert => cert, + :key_path => path[:key], :cert_path => path[:cert] } + end + + # + # Add certificate to trusted cert list. + # + # Note: At the moment these are stored in OPT[:trust_dir], although that + # directory may change in the future. + # + def self.add_trusted_cert(cert, opt = {}) + opt = OPT.merge(opt) + + # get destination path + path = Gem::Security::Policy.trusted_cert_path(cert, opt) + + # verify trust directory (can't write to nowhere, you know) + verify_trust_dir(opt[:trust_dir], opt[:perms][:trust_dir]) + + # write cert to output file + File.open(path, 'wb') do |file| + file.chmod(opt[:perms][:trusted_cert]) + file.write(cert.to_pem) + end + + # return nil + nil + end + + # + # Basic OpenSSL-based package signing class. + # + class Signer + attr_accessor :key, :cert_chain + + def initialize(key, cert_chain) + Gem.ensure_ssl_available + @algo = Gem::Security::OPT[:dgst_algo] + @key, @cert_chain = key, cert_chain + + # check key, if it's a file, and if it's key, leave it alone + if @key && !@key.kind_of?(OpenSSL::PKey::PKey) + @key = OpenSSL::PKey::RSA.new(File.read(@key)) + end + + # check cert chain, if it's a file, load it, if it's cert data, convert + # it into a cert object, and if it's a cert object, leave it alone + if @cert_chain + @cert_chain = @cert_chain.map do |cert| + # check cert, if it's a file, load it, if it's cert data, convert it + # into a cert object, and if it's a cert object, leave it alone + if cert && !cert.kind_of?(OpenSSL::X509::Certificate) + cert = File.read(cert) if File::exist?(cert) + cert = OpenSSL::X509::Certificate.new(cert) + end + cert + end + end + end + + # + # Sign data with given digest algorithm + # + def sign(data) + @key.sign(@algo.new, data) + end + + end +end + diff --git a/lib/rubygems/server.rb b/lib/rubygems/server.rb new file mode 100644 index 0000000000..212ccc5f7e --- /dev/null +++ b/lib/rubygems/server.rb @@ -0,0 +1,504 @@ +require 'webrick' +require 'rdoc/template' +require 'yaml' +require 'zlib' + +require 'rubygems' + +## +# Gem::Server and allows users to serve gems for consumption by +# `gem --remote-install`. +# +# gem_server starts an HTTP server on the given port and serves the folowing: +# * "/" - Browsing of gem spec files for installed gems +# * "/Marshal" - Full SourceIndex dump of metadata for installed gems +# * "/yaml" - YAML dump of metadata for installed gems - deprecated +# * "/gems" - Direct access to download the installable gems +# +# == Usage +# +# gem server [-p portnum] [-d gem_path] +# +# port_num:: The TCP port the HTTP server will bind to +# gem_path:: +# Root gem directory containing both "cache" and "specifications" +# subdirectories. +class Gem::Server + + include Gem::UserInteraction + + DOC_TEMPLATE = <<-WEBPAGE + + + + + + RubyGems Documentation Index + + + + +
+

RubyGems Documentation Index

+
+ + +
+
+
+

Summary

+

There are %gem_count% gems installed:

+

+START:specs +IFNOT:is_last +%name%, +ENDIF:is_last +IF:is_last +%name%. +ENDIF:is_last +END:specs +

Gems

+ +
+START:specs +
+IF:first_name_entry + +ENDIF:first_name_entry +%name% %version% +IF:rdoc_installed + [rdoc] +ENDIF:rdoc_installed +IFNOT:rdoc_installed + [rdoc] +ENDIF:rdoc_installed +IF:homepage +[www] +ENDIF:homepage +IFNOT:homepage +[www] +ENDIF:homepage +IF:has_deps + - depends on +START:dependencies +IFNOT:is_last +%name%, +ENDIF:is_last +IF:is_last +%name%. +ENDIF:is_last +END:dependencies +ENDIF:has_deps +
+
+%summary% +IF:executables +
+ +IF:only_one_executable + Executable is +ENDIF:only_one_executable + +IFNOT:only_one_executable + Executables are +ENDIF:only_one_executable + +START:executables +IFNOT:is_last + %executable%, +ENDIF:is_last +IF:is_last + %executable%. +ENDIF:is_last +END:executables +ENDIF:executables +
+
+
+END:specs +
+ +
+
+
+ + + + WEBPAGE + + # CSS is copy & paste from rdoc-style.css, RDoc V1.0.1 - 20041108 + RDOC_CSS = <<-RDOCCSS +body { + font-family: Verdana,Arial,Helvetica,sans-serif; + font-size: 90%; + margin: 0; + margin-left: 40px; + padding: 0; + background: white; +} + +h1,h2,h3,h4 { margin: 0; color: #efefef; background: transparent; } +h1 { font-size: 150%; } +h2,h3,h4 { margin-top: 1em; } + +a { background: #eef; color: #039; text-decoration: none; } +a:hover { background: #039; color: #eef; } + +/* Override the base stylesheets Anchor inside a table cell */ +td > a { + background: transparent; + color: #039; + text-decoration: none; +} + +/* and inside a section title */ +.section-title > a { + background: transparent; + color: #eee; + text-decoration: none; +} + +/* === Structural elements =================================== */ + +div#index { + margin: 0; + margin-left: -40px; + padding: 0; + font-size: 90%; +} + + +div#index a { + margin-left: 0.7em; +} + +div#index .section-bar { + margin-left: 0px; + padding-left: 0.7em; + background: #ccc; + font-size: small; +} + + +div#classHeader, div#fileHeader { + width: auto; + color: white; + padding: 0.5em 1.5em 0.5em 1.5em; + margin: 0; + margin-left: -40px; + border-bottom: 3px solid #006; +} + +div#classHeader a, div#fileHeader a { + background: inherit; + color: white; +} + +div#classHeader td, div#fileHeader td { + background: inherit; + color: white; +} + + +div#fileHeader { + background: #057; +} + +div#classHeader { + background: #048; +} + + +.class-name-in-header { + font-size: 180%; + font-weight: bold; +} + + +div#bodyContent { + padding: 0 1.5em 0 1.5em; +} + +div#description { + padding: 0.5em 1.5em; + background: #efefef; + border: 1px dotted #999; +} + +div#description h1,h2,h3,h4,h5,h6 { + color: #125;; + background: transparent; +} + +div#validator-badges { + text-align: center; +} +div#validator-badges img { border: 0; } + +div#copyright { + color: #333; + background: #efefef; + font: 0.75em sans-serif; + margin-top: 5em; + margin-bottom: 0; + padding: 0.5em 2em; +} + + +/* === Classes =================================== */ + +table.header-table { + color: white; + font-size: small; +} + +.type-note { + font-size: small; + color: #DEDEDE; +} + +.xxsection-bar { + background: #eee; + color: #333; + padding: 3px; +} + +.section-bar { + color: #333; + border-bottom: 1px solid #999; + margin-left: -20px; +} + + +.section-title { + background: #79a; + color: #eee; + padding: 3px; + margin-top: 2em; + margin-left: -30px; + border: 1px solid #999; +} + +.top-aligned-row { vertical-align: top } +.bottom-aligned-row { vertical-align: bottom } + +/* --- Context section classes ----------------------- */ + +.context-row { } +.context-item-name { font-family: monospace; font-weight: bold; color: black; } +.context-item-value { font-size: small; color: #448; } +.context-item-desc { color: #333; padding-left: 2em; } + +/* --- Method classes -------------------------- */ +.method-detail { + background: #efefef; + padding: 0; + margin-top: 0.5em; + margin-bottom: 1em; + border: 1px dotted #ccc; +} +.method-heading { + color: black; + background: #ccc; + border-bottom: 1px solid #666; + padding: 0.2em 0.5em 0 0.5em; +} +.method-signature { color: black; background: inherit; } +.method-name { font-weight: bold; } +.method-args { font-style: italic; } +.method-description { padding: 0 0.5em 0 0.5em; } + +/* --- Source code sections -------------------- */ + +a.source-toggle { font-size: 90%; } +div.method-source-code { + background: #262626; + color: #ffdead; + margin: 1em; + padding: 0.5em; + border: 1px dashed #999; + overflow: hidden; +} + +div.method-source-code pre { color: #ffdead; overflow: hidden; } + +/* --- Ruby keyword styles --------------------- */ + +.standalone-code { background: #221111; color: #ffdead; overflow: hidden; } + +.ruby-constant { color: #7fffd4; background: transparent; } +.ruby-keyword { color: #00ffff; background: transparent; } +.ruby-ivar { color: #eedd82; background: transparent; } +.ruby-operator { color: #00ffee; background: transparent; } +.ruby-identifier { color: #ffdead; background: transparent; } +.ruby-node { color: #ffa07a; background: transparent; } +.ruby-comment { color: #b22222; font-weight: bold; background: transparent; } +.ruby-regexp { color: #ffa07a; background: transparent; } +.ruby-value { color: #7fffd4; background: transparent; } + RDOCCSS + + def self.run(options) + new(options[:gemdir], options[:port], options[:daemon]).run + end + + def initialize(gemdir, port, daemon) + Socket.do_not_reverse_lookup = true + + @gemdir = gemdir + @port = port + @daemon = daemon + logger = WEBrick::Log.new nil, WEBrick::BasicLog::FATAL + @server = WEBrick::HTTPServer.new :DoNotListen => true, :Logger => logger + + @spec_dir = File.join @gemdir, "specifications" + @source_index = Gem::SourceIndex.from_gems_in @spec_dir + end + + def quick(req, res) + res['content-type'] = 'text/plain' + res['date'] = File.stat(@spec_dir).mtime + + case req.request_uri.request_uri + when '/quick/index' then + res.body << @source_index.map { |name,_| name }.join("\n") + when '/quick/index.rz' then + index = @source_index.map { |name,_| name }.join("\n") + res.body << Zlib::Deflate.deflate(index) + when %r|^/quick/(.*)-([0-9.]+)\.gemspec(\.marshal)?\.rz$| then + specs = @source_index.search $1, $2 + if specs.empty? then + res.status = 404 + elsif specs.length > 1 then + res.status = 500 + elsif $3 # marshal quickindex instead of YAML + res.body << Zlib::Deflate.deflate(Marshal.dump(specs.first)) + else # deprecated YAML format + res.body << Zlib::Deflate.deflate(specs.first.to_yaml) + end + else + res.status = 404 + end + end + + def run + @server.listen nil, @port + + say "Starting gem server on http://localhost:#{@port}/" + + WEBrick::Daemon.start if @daemon + + @server.mount_proc("/yaml") do |req, res| + res['content-type'] = 'text/plain' + res['date'] = File.stat(@spec_dir).mtime + if req.request_method == 'HEAD' then + res['content-length'] = @source_index.to_yaml.length + else + res.body << @source_index.to_yaml + end + end + + @server.mount_proc("/Marshal") do |req, res| + res['content-type'] = 'text/plain' + res['date'] = File.stat(@spec_dir).mtime + if req.request_method == 'HEAD' then + res['content-length'] = Marshal.dump(@source_index).length + else + res.body << Marshal.dump(@source_index) + end + end + + @server.mount_proc("/quick/", &method(:quick)) + + @server.mount_proc("/gem-server-rdoc-style.css") do |req, res| + res['content-type'] = 'text/css' + res['date'] = File.stat(@spec_dir).mtime + res.body << RDOC_CSS + end + + @server.mount_proc("/") do |req, res| + specs = [] + total_file_count = 0 + + @source_index.each do |path, spec| + total_file_count += spec.files.size + deps = spec.dependencies.collect { |dep| + { "name" => dep.name, + "version" => dep.version_requirements.to_s, } + } + deps = deps.sort_by { |dep| [dep["name"].downcase, dep["version"]] } + deps.last["is_last"] = true unless deps.empty? + + # executables + executables = spec.executables.sort.collect { |exec| {"executable" => exec} } + executables = nil if executables.empty? + executables.last["is_last"] = true if executables + + specs << { + "authors" => spec.authors.sort.join(", "), + "date" => spec.date.to_s, + "dependencies" => deps, + "doc_path" => ('/doc_root/' + spec.full_name + '/rdoc/index.html'), + "executables" => executables, + "only_one_executable" => (executables && executables.size==1), + "full_name" => spec.full_name, + "has_deps" => !deps.empty?, + "homepage" => spec.homepage, + "name" => spec.name, + "rdoc_installed" => Gem::DocManager.new(spec).rdoc_installed?, + "summary" => spec.summary, + "version" => spec.version.to_s, + } + end + + specs << { + "authors" => "Chad Fowler, Rich Kilmer, Jim Weirich, Eric Hodel and others", + "dependencies" => [], + "doc_path" => "/doc_root/rubygems-#{Gem::RubyGemsVersion}/rdoc/index.html", + "executables" => [{"executable" => 'gem', "is_last" => true}], + "only_one_executable" => true, + "full_name" => "rubygems-#{Gem::RubyGemsVersion}", + "has_deps" => false, + "homepage" => "http://rubygems.org/", + "name" => 'rubygems', + "rdoc_installed" => true, + "summary" => "RubyGems itself", + "version" => Gem::RubyGemsVersion, + } + + specs = specs.sort_by { |spec| [spec["name"].downcase, spec["version"]] } + specs.last["is_last"] = true + + # tag all specs with first_name_entry + last_spec = nil + specs.each do |spec| + is_first = last_spec.nil? || (last_spec["name"].downcase != spec["name"].downcase) + spec["first_name_entry"] = is_first + last_spec = spec + end + + # create page from template + template = TemplatePage.new(DOC_TEMPLATE) + res['content-type'] = 'text/html' + template.write_html_on res.body, + "gem_count" => specs.size.to_s, "specs" => specs, + "total_file_count" => total_file_count.to_s + end + + paths = { "/gems" => "/cache/", "/doc_root" => "/doc/" } + paths.each do |mount_point, mount_dir| + @server.mount(mount_point, WEBrick::HTTPServlet::FileHandler, + File.join(@gemdir, mount_dir), true) + end + + trap("INT") { @server.shutdown; exit! } + trap("TERM") { @server.shutdown; exit! } + + @server.start + end + +end + diff --git a/lib/rubygems/source_index.rb b/lib/rubygems/source_index.rb new file mode 100644 index 0000000000..759718d45c --- /dev/null +++ b/lib/rubygems/source_index.rb @@ -0,0 +1,446 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'forwardable' + +require 'rubygems' +require 'rubygems/user_interaction' +require 'rubygems/specification' + +module Gem + + # The SourceIndex object indexes all the gems available from a + # particular source (e.g. a list of gem directories, or a remote + # source). A SourceIndex maps a gem full name to a gem + # specification. + # + # NOTE:: The class used to be named Cache, but that became + # confusing when cached source fetchers where introduced. The + # constant Gem::Cache is an alias for this class to allow old + # YAMLized source index objects to load properly. + # + class SourceIndex + extend Forwardable + + include Enumerable + + include Gem::UserInteraction + + # Class Methods. ------------------------------------------------- + class << self + include Gem::UserInteraction + + # Factory method to construct a source index instance for a given + # path. + # + # deprecated:: + # If supplied, from_installed_gems will act just like + # +from_gems_in+. This argument is deprecated and is provided + # just for backwards compatibility, and should not generally + # be used. + # + # return:: + # SourceIndex instance + # + def from_installed_gems(*deprecated) + if deprecated.empty? + from_gems_in(*installed_spec_directories) + else + from_gems_in(*deprecated) + end + end + + # Return a list of directories in the current gem path that + # contain specifications. + # + # return:: + # List of directory paths (all ending in "../specifications"). + # + def installed_spec_directories + Gem.path.collect { |dir| File.join(dir, "specifications") } + end + + # Factory method to construct a source index instance for a + # given path. + # + # spec_dirs:: + # List of directories to search for specifications. Each + # directory should have a "specifications" subdirectory + # containing the gem specifications. + # + # return:: + # SourceIndex instance + # + def from_gems_in(*spec_dirs) + self.new.load_gems_in(*spec_dirs) + end + + # Load a specification from a file (eval'd Ruby code) + # + # file_name:: [String] The .gemspec file + # return:: Specification instance or nil if an error occurs + # + def load_specification(file_name) + begin + spec_code = File.read(file_name).untaint + gemspec = eval spec_code, binding, file_name + if gemspec.is_a?(Gem::Specification) + gemspec.loaded_from = file_name + return gemspec + end + alert_warning "File '#{file_name}' does not evaluate to a gem specification" + rescue SyntaxError => e + alert_warning e + alert_warning spec_code + rescue Exception => e + alert_warning(e.inspect.to_s + "\n" + spec_code) + alert_warning "Invalid .gemspec format in '#{file_name}'" + end + return nil + end + + end + + # Instance Methods ----------------------------------------------- + + # Constructs a source index instance from the provided + # specifications + # + # specifications:: + # [Hash] hash of [Gem name, Gem::Specification] pairs + # + def initialize(specifications={}) + @gems = specifications + end + + # Reconstruct the source index from the list of source + # directories. + def load_gems_in(*spec_dirs) + @gems.clear + specs = Dir.glob File.join("{#{spec_dirs.join(',')}}", "*.gemspec") + specs.each do |file_name| + gemspec = self.class.load_specification(file_name.untaint) + add_spec(gemspec) if gemspec + end + self + end + + # Returns a Hash of name => Specification of the latest versions of each + # gem in this index. + def latest_specs + result, latest = Hash.new { |h,k| h[k] = [] }, {} + + self.each do |_, spec| # SourceIndex is not a hash, so we're stuck with each + name = spec.name + curr_ver = spec.version + prev_ver = latest[name] + + next unless prev_ver.nil? or curr_ver >= prev_ver + + if prev_ver.nil? or curr_ver > prev_ver then + result[name].clear + latest[name] = curr_ver + end + + result[name] << spec + end + + result.values.flatten + end + + # Add a gem specification to the source index. + def add_spec(gem_spec) + @gems[gem_spec.full_name] = gem_spec + end + + # Remove a gem specification named +full_name+. + def remove_spec(full_name) + @gems.delete(full_name) + end + + # Iterate over the specifications in the source index. + def each(&block) # :yields: gem.full_name, gem + @gems.each(&block) + end + + # The gem specification given a full gem spec name. + def specification(full_name) + @gems[full_name] + end + + # The signature for the source index. Changes in the signature + # indicate a change in the index. + def index_signature + require 'rubygems/digest/sha2' + + Gem::SHA256.new.hexdigest(@gems.keys.sort.join(',')).to_s + end + + # The signature for the given gem specification. + def gem_signature(gem_full_name) + require 'rubygems/digest/sha2' + + Gem::SHA256.new.hexdigest(@gems[gem_full_name].to_yaml).to_s + end + + def_delegators :@gems, :size, :length + + # Find a gem by an exact match on the short name. + def find_name(gem_name, version_requirement = Gem::Requirement.default) + search(/^#{gem_name}$/, version_requirement) + end + + # Search for a gem by short name pattern and optional version + # + # gem_name:: + # [String] a partial for the (short) name of the gem, or + # [Regex] a pattern to match against the short name + # version_requirement:: + # [String | default=Gem::Requirement.default] version to + # find + # return:: + # [Array] list of Gem::Specification objects in sorted (version) + # order. Empty if not found. + # + def search(gem_pattern, platform_only_or_version_req = false) + version_requirement = nil + only_platform = false + + case gem_pattern + when Regexp then + version_requirement = platform_only_or_version_req || + Gem::Requirement.default + when Gem::Dependency then + only_platform = platform_only_or_version_req + version_requirement = gem_pattern.version_requirements + gem_pattern = gem_pattern.name.empty? ? // : /^#{gem_pattern.name}$/ + else + version_requirement = platform_only_or_version_req || + Gem::Requirement.default + gem_pattern = /#{gem_pattern}/i + end + + unless Gem::Requirement === version_requirement then + version_requirement = Gem::Requirement.create version_requirement + end + + specs = @gems.values.select do |spec| + spec.name =~ gem_pattern and + version_requirement.satisfied_by? spec.version + end + + if only_platform then + specs = specs.select do |spec| + Gem::Platform.match spec.platform + end + end + + specs.sort_by { |s| s.sort_obj } + end + + # Refresh the source index from the local file system. + # + # return:: Returns a pointer to itself. + # + def refresh! + load_gems_in(self.class.installed_spec_directories) + end + + # Returns an Array of Gem::Specifications that are not up to date. + # + def outdated + dep = Gem::Dependency.new '', Gem::Requirement.default + + remotes = Gem::SourceInfoCache.search dep, true + + outdateds = [] + + latest_specs.each do |local| + name = local.name + remote = remotes.select { |spec| spec.name == name }. + sort_by { |spec| spec.version.to_ints }. + last + outdateds << name if remote and local.version < remote.version + end + + outdateds + end + + def update(source_uri) + use_incremental = false + + begin + gem_names = fetch_quick_index source_uri + remove_extra gem_names + missing_gems = find_missing gem_names + + return false if missing_gems.size.zero? + + say "missing #{missing_gems.size} gems" if + missing_gems.size > 0 and Gem.configuration.really_verbose + + use_incremental = missing_gems.size <= Gem.configuration.bulk_threshold + rescue Gem::OperationNotSupportedError => ex + alert_error "Falling back to bulk fetch: #{ex.message}" if + Gem.configuration.really_verbose + use_incremental = false + end + + if use_incremental then + update_with_missing(source_uri, missing_gems) + else + new_index = fetch_bulk_index(source_uri) + @gems.replace(new_index.gems) + end + + true + end + + def ==(other) # :nodoc: + self.class === other and @gems == other.gems + end + + def dump + Marshal.dump(self) + end + + protected + + attr_reader :gems + + private + + def fetcher + require 'rubygems/remote_fetcher' + + Gem::RemoteFetcher.fetcher + end + + def fetch_index_from(source_uri) + @fetch_error = nil + + indexes = %W[ + Marshal.#{Gem.marshal_version}.Z + Marshal.#{Gem.marshal_version} + yaml.Z + yaml + ] + + indexes.each do |name| + spec_data = nil + begin + spec_data = fetcher.fetch_path("#{source_uri}/#{name}") + spec_data = unzip(spec_data) if name =~ /\.Z$/ + if name =~ /Marshal/ then + return Marshal.load(spec_data) + else + return YAML.load(spec_data) + end + rescue => e + if Gem.configuration.really_verbose then + alert_error "Unable to fetch #{name}: #{e.message}" + end + @fetch_error = e + end + end + nil + end + + def fetch_bulk_index(source_uri) + say "Bulk updating Gem source index for: #{source_uri}" + + index = fetch_index_from(source_uri) + if index.nil? then + raise Gem::RemoteSourceException, + "Error fetching remote gem cache: #{@fetch_error}" + end + @fetch_error = nil + index + end + + # Get the quick index needed for incremental updates. + def fetch_quick_index(source_uri) + zipped_index = fetcher.fetch_path source_uri + '/quick/index.rz' + unzip(zipped_index).split("\n") + rescue ::Exception => ex + raise Gem::OperationNotSupportedError, + "No quick index found: " + ex.message + end + + # Make a list of full names for all the missing gemspecs. + def find_missing(spec_names) + spec_names.find_all { |full_name| + specification(full_name).nil? + } + end + + def remove_extra(spec_names) + dictionary = spec_names.inject({}) { |h, k| h[k] = true; h } + each do |name, spec| + remove_spec name unless dictionary.include? name + end + end + + # Unzip the given string. + def unzip(string) + require 'zlib' + Zlib::Inflate.inflate(string) + end + + # Tries to fetch Marshal representation first, then YAML + def fetch_single_spec(source_uri, spec_name) + @fetch_error = nil + begin + marshal_uri = source_uri + "/quick/Marshal.#{Gem.marshal_version}/#{spec_name}.gemspec.rz" + zipped = fetcher.fetch_path marshal_uri + return Marshal.load(unzip(zipped)) + rescue => ex + @fetch_error = ex + if Gem.configuration.really_verbose then + say "unable to fetch marshal gemspec #{marshal_uri}: #{ex.class} - #{ex}" + end + end + + begin + yaml_uri = source_uri + "/quick/#{spec_name}.gemspec.rz" + zipped = fetcher.fetch_path yaml_uri + return YAML.load(unzip(zipped)) + rescue => ex + @fetch_error = ex + if Gem.configuration.really_verbose then + say "unable to fetch YAML gemspec #{yaml_uri}: #{ex.class} - #{ex}" + end + end + nil + end + + # Update the cached source index with the missing names. + def update_with_missing(source_uri, missing_names) + progress = ui.progress_reporter(missing_names.size, + "Updating metadata for #{missing_names.size} gems from #{source_uri}") + missing_names.each do |spec_name| + gemspec = fetch_single_spec(source_uri, spec_name) + if gemspec.nil? then + ui.say "Failed to download spec #{spec_name} from #{source_uri}:\n" \ + "\t#{@fetch_error.message}" + else + add_spec gemspec + progress.updated spec_name + end + @fetch_error = nil + end + progress.done + progress.count + end + + end + + # Cache is an alias for SourceIndex to allow older YAMLized source + # index objects to load properly. + Cache = SourceIndex + +end + diff --git a/lib/rubygems/source_info_cache.rb b/lib/rubygems/source_info_cache.rb new file mode 100644 index 0000000000..0498e895a4 --- /dev/null +++ b/lib/rubygems/source_info_cache.rb @@ -0,0 +1,232 @@ +require 'fileutils' + +require 'rubygems' +require 'rubygems/source_info_cache_entry' +require 'rubygems/user_interaction' + +# SourceInfoCache stores a copy of the gem index for each gem source. +# +# There are two possible cache locations, the system cache and the user cache: +# * The system cache is prefered if it is writable or can be created. +# * The user cache is used otherwise +# +# Once a cache is selected, it will be used for all operations. +# SourceInfoCache will not switch between cache files dynamically. +# +# Cache data is a Hash mapping a source URI to a SourceInfoCacheEntry. +# +#-- +# To keep things straight, this is how the cache objects all fit together: +# +# Gem::SourceInfoCache +# @cache_data = { +# source_uri => Gem::SourceInfoCacheEntry +# @size => source index size +# @source_index => Gem::SourceIndex +# ... +# } +# +class Gem::SourceInfoCache + + include Gem::UserInteraction + + @cache = nil + @system_cache_file = nil + @user_cache_file = nil + + def self.cache + return @cache if @cache + @cache = new + @cache.refresh if Gem.configuration.update_sources + @cache + end + + def self.cache_data + cache.cache_data + end + + # Search all source indexes for +pattern+. + def self.search(pattern, platform_only = false) + cache.search pattern, platform_only + end + + # Search all source indexes for +pattern+. Only returns gems matching + # Gem.platforms when +only_platform+ is true. See #search_with_source. + def self.search_with_source(pattern, only_platform = false) + cache.search_with_source(pattern, only_platform) + end + + def initialize # :nodoc: + @cache_data = nil + @cache_file = nil + @dirty = false + end + + # The most recent cache data. + def cache_data + return @cache_data if @cache_data + cache_file # HACK writable check + + begin + # Marshal loads 30-40% faster from a String, and 2MB on 20061116 is small + data = File.open cache_file, 'rb' do |fp| fp.read end + @cache_data = Marshal.load data + + @cache_data.each do |url, sice| + next unless sice.is_a?(Hash) + update + cache = sice['cache'] + size = sice['size'] + if cache.is_a?(Gem::SourceIndex) and size.is_a?(Numeric) then + new_sice = Gem::SourceInfoCacheEntry.new cache, size + @cache_data[url] = new_sice + else # irreperable, force refetch. + reset_cache_for(url) + end + end + @cache_data + rescue => e + if Gem.configuration.really_verbose then + say "Exception during cache_data handling: #{ex.class} - #{ex}" + say "Cache file was: #{cache_file}" + say "\t#{e.backtrace.join "\n\t"}" + end + reset_cache_data + end + end + + def reset_cache_for(url) + say "Reseting cache for #{url}" if Gem.configuration.really_verbose + + sice = Gem::SourceInfoCacheEntry.new Gem::SourceIndex.new, 0 + sice.refresh url # HACK may be unnecessary, see ::cache and #refresh + + @cache_data[url] = sice + @cache_data + end + + def reset_cache_data + @cache_data = {} + end + + # The name of the cache file to be read + def cache_file + return @cache_file if @cache_file + @cache_file = (try_file(system_cache_file) or + try_file(user_cache_file) or + raise "unable to locate a writable cache file") + end + + # Write the cache to a local file (if it is dirty). + def flush + write_cache if @dirty + @dirty = false + end + + # Refreshes each source in the cache from its repository. + def refresh + Gem.sources.each do |source_uri| + cache_entry = cache_data[source_uri] + if cache_entry.nil? then + cache_entry = Gem::SourceInfoCacheEntry.new nil, 0 + cache_data[source_uri] = cache_entry + end + + update if cache_entry.refresh source_uri + end + + flush + end + + # Searches all source indexes for +pattern+. + def search(pattern, platform_only = false) + cache_data.map do |source_uri, sic_entry| + next unless Gem.sources.include? source_uri + sic_entry.source_index.search pattern, platform_only + end.flatten.compact + end + + # Searches all source indexes for +pattern+. If +only_platform+ is true, + # only gems matching Gem.platforms will be selected. Returns an Array of + # pairs containing the Gem::Specification found and the source_uri it was + # found at. + def search_with_source(pattern, only_platform = false) + results = [] + + cache_data.map do |source_uri, sic_entry| + next unless Gem.sources.include? source_uri + + sic_entry.source_index.search(pattern, only_platform).each do |spec| + results << [spec, source_uri] + end + end + + results + end + + # Mark the cache as updated (i.e. dirty). + def update + @dirty = true + end + + # The name of the system cache file. + def system_cache_file + self.class.system_cache_file + end + + # The name of the system cache file. (class method) + def self.system_cache_file + @system_cache_file ||= File.join(Gem.dir, "source_cache") + end + + # The name of the user cache file. + def user_cache_file + self.class.user_cache_file + end + + # The name of the user cache file. (class method) + def self.user_cache_file + @user_cache_file ||= + ENV['GEMCACHE'] || File.join(Gem.user_home, ".gem", "source_cache") + end + + # Write data to the proper cache. + def write_cache + open cache_file, "wb" do |f| + f.write Marshal.dump(cache_data) + end + end + + # Set the source info cache data directly. This is mainly used for unit + # testing when we don't want to read a file system to grab the cached source + # index information. The +hash+ should map a source URL into a + # SourceInfoCacheEntry. + def set_cache_data(hash) + @cache_data = hash + update + end + + private + + # Determine if +fn+ is a candidate for a cache file. Return fn if + # it is. Return nil if it is not. + def try_file(fn) + return fn if File.writable?(fn) + return nil if File.exist?(fn) + dir = File.dirname(fn) + unless File.exist? dir then + begin + FileUtils.mkdir_p(dir) + rescue RuntimeError + return nil + end + end + if File.writable?(dir) + File.open(fn, "wb") { |f| f << Marshal.dump({}) } + return fn + end + nil + end + +end + diff --git a/lib/rubygems/source_info_cache_entry.rb b/lib/rubygems/source_info_cache_entry.rb new file mode 100644 index 0000000000..02e03ca9db --- /dev/null +++ b/lib/rubygems/source_info_cache_entry.rb @@ -0,0 +1,46 @@ +require 'rubygems' +require 'rubygems/source_index' +require 'rubygems/remote_fetcher' + +## +# Entrys held by a SourceInfoCache. + +class Gem::SourceInfoCacheEntry + + # The source index for this cache entry. + attr_reader :source_index + + # The size of the of the source entry. Used to determine if the + # source index has changed. + attr_reader :size + + # Create a cache entry. + def initialize(si, size) + @source_index = si || Gem::SourceIndex.new({}) + @size = size + end + + def refresh(source_uri) + begin + marshal_uri = URI.join source_uri.to_s, "Marshal.#{Gem.marshal_version}" + remote_size = Gem::RemoteFetcher.fetcher.fetch_size marshal_uri + rescue Gem::RemoteSourceException + yaml_uri = URI.join source_uri.to_s, 'yaml' + remote_size = Gem::RemoteFetcher.fetcher.fetch_size yaml_uri + end + + return false if @size == remote_size # TODO Use index_signature instead of size? + updated = @source_index.update source_uri + @size = remote_size + + updated + end + + def ==(other) # :nodoc: + self.class === other and + @size == other.size and + @source_index == other.source_index + end + +end + diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb new file mode 100644 index 0000000000..308ed717a4 --- /dev/null +++ b/lib/rubygems/specification.rb @@ -0,0 +1,905 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'time' +require 'rubygems' +require 'rubygems/version' +require 'rubygems/platform' + +# :stopdoc: +# Time::today has been deprecated in 0.9.5 and will be removed. +def Time.today + t = Time.now + t - ((t.to_i + t.gmt_offset) % 86400) +end unless defined? Time.today +# :startdoc: + +module Gem + + # == Gem::Specification + # + # The Specification class contains the metadata for a Gem. Typically + # defined in a .gemspec file or a Rakefile, and looks like this: + # + # spec = Gem::Specification.new do |s| + # s.name = 'rfoo' + # s.version = '1.0' + # s.summary = 'Example gem specification' + # ... + # end + # + # There are many gemspec attributes, and the best place to learn + # about them in the "Gemspec Reference" linked from the RubyGems wiki. + # + class Specification + + # Allows deinstallation of gems with legacy platforms. + attr_accessor :original_platform # :nodoc: + + # ------------------------- Specification version contstants. + + # The the version number of a specification that does not specify one + # (i.e. RubyGems 0.7 or earlier). + NONEXISTENT_SPECIFICATION_VERSION = -1 + + # The specification version applied to any new Specification instances + # created. This should be bumped whenever something in the spec format + # changes. + CURRENT_SPECIFICATION_VERSION = 2 + + # An informal list of changes to the specification. The highest-valued + # key should be equal to the CURRENT_SPECIFICATION_VERSION. + SPECIFICATION_VERSION_HISTORY = { + -1 => ['(RubyGems versions up to and including 0.7 did not have versioned specifications)'], + 1 => [ + 'Deprecated "test_suite_file" in favor of the new, but equivalent, "test_files"', + '"test_file=x" is a shortcut for "test_files=[x]"' + ], + 2 => [ + 'Added "required_rubygems_version"', + 'Now forward-compatible with future versions', + ], + } + + # :stopdoc: + MARSHAL_FIELDS = { -1 => 16, 1 => 16, 2 => 16 } + + now = Time.at(Time.now.to_i) + TODAY = now - ((now.to_i + now.gmt_offset) % 86400) + # :startdoc: + + # ------------------------- Class variables. + + # List of Specification instances. + @@list = [] + + # Optional block used to gather newly defined instances. + @@gather = nil + + # List of attribute names: [:name, :version, ...] + @@required_attributes = [] + + # List of _all_ attributes and default values: [[:name, nil], [:bindir, 'bin'], ...] + @@attributes = [] + + @@nil_attributes = [] + @@non_nil_attributes = [:@original_platform] + + # List of array attributes + @@array_attributes = [] + + # Map of attribute names to default values. + @@default_value = {} + + # ------------------------- Convenience class methods. + + def self.attribute_names + @@attributes.map { |name, default| name } + end + + def self.attribute_defaults + @@attributes.dup + end + + def self.default_value(name) + @@default_value[name] + end + + def self.required_attributes + @@required_attributes.dup + end + + def self.required_attribute?(name) + @@required_attributes.include? name.to_sym + end + + def self.array_attributes + @@array_attributes.dup + end + + # ------------------------- Infrastructure class methods. + + # A list of Specification instances that have been defined in this Ruby instance. + def self.list + @@list + end + + # Used to specify the name and default value of a specification + # attribute. The side effects are: + # * the name and default value are added to the @@attributes list + # and @@default_value map + # * a standard _writer_ method (attribute=) is created + # * a non-standard _reader method (attribute) is created + # + # The reader method behaves like this: + # def attribute + # @attribute ||= (copy of default value) + # end + # + # This allows lazy initialization of attributes to their default + # values. + # + def self.attribute(name, default=nil) + ivar_name = "@#{name}".intern + if default.nil? then + @@nil_attributes << ivar_name + else + @@non_nil_attributes << [ivar_name, default] + end + + @@attributes << [name, default] + @@default_value[name] = default + attr_accessor(name) + end + + # Same as :attribute, but ensures that values assigned to the + # attribute are array values by applying :to_a to the value. + def self.array_attribute(name) + @@non_nil_attributes << ["@#{name}".intern, []] + + @@array_attributes << name + @@attributes << [name, []] + @@default_value[name] = [] + code = %{ + def #{name} + @#{name} ||= [] + end + def #{name}=(value) + @#{name} = Array(value) + end + } + + module_eval code, __FILE__, __LINE__ - 9 + end + + # Same as attribute above, but also records this attribute as mandatory. + def self.required_attribute(*args) + @@required_attributes << args.first + attribute(*args) + end + + # Sometimes we don't want the world to use a setter method for a particular attribute. + # +read_only+ makes it private so we can still use it internally. + def self.read_only(*names) + names.each do |name| + private "#{name}=" + end + end + + # Shortcut for creating several attributes at once (each with a default value of + # +nil+). + def self.attributes(*args) + args.each do |arg| + attribute(arg, nil) + end + end + + # Some attributes require special behaviour when they are accessed. This allows for + # that. + def self.overwrite_accessor(name, &block) + remove_method name + define_method(name, &block) + end + + # Defines a _singular_ version of an existing _plural_ attribute + # (i.e. one whose value is expected to be an array). This means + # just creating a helper method that takes a single value and + # appends it to the array. These are created for convenience, so + # that in a spec, one can write + # + # s.require_path = 'mylib' + # + # instead of + # + # s.require_paths = ['mylib'] + # + # That above convenience is available courtesy of + # + # attribute_alias_singular :require_path, :require_paths + # + def self.attribute_alias_singular(singular, plural) + define_method("#{singular}=") { |val| + send("#{plural}=", [val]) + } + define_method("#{singular}") { + val = send("#{plural}") + val.nil? ? nil : val.first + } + end + + # Dump only crucial instance variables. + # + # MAINTAIN ORDER! + def _dump(limit) # :nodoc: + Marshal.dump [ + @rubygems_version, + @specification_version, + @name, + @version, + (Time === @date ? @date : Time.parse(@date.to_s)), + @summary, + @required_ruby_version, + @required_rubygems_version, + @new_platform, + @dependencies, + @rubyforge_project, + @email, + @authors, + @description, + @homepage, + @has_rdoc + ] + end + + # Load custom marshal format, re-initializing defaults as needed + def self._load(str) + array = Marshal.load str + + spec = Gem::Specification.new + spec.instance_variable_set :@specification_version, array[1] + + current_version = CURRENT_SPECIFICATION_VERSION + + field_count = MARSHAL_FIELDS[spec.specification_version] + + if field_count.nil? or array.size < field_count then + raise TypeError, "invalid Gem::Specification format #{array.inspect}" + end + + spec.instance_variable_set :@rubygems_version, array[0] + # spec version + spec.instance_variable_set :@name, array[2] + spec.instance_variable_set :@version, array[3] + spec.instance_variable_set :@date, array[4] + spec.instance_variable_set :@summary, array[5] + spec.instance_variable_set :@required_ruby_version, array[6] + spec.instance_variable_set :@required_rubygems_version, array[7] + spec.instance_variable_set :@new_platform, array[8] + spec.instance_variable_set :@original_platform, array[8] + spec.instance_variable_set :@platform, array[8].to_s + spec.instance_variable_set :@dependencies, array[9] + spec.instance_variable_set :@rubyforge_project, array[10] + spec.instance_variable_set :@email, array[11] + spec.instance_variable_set :@authors, array[12] + spec.instance_variable_set :@description, array[13] + spec.instance_variable_set :@homepage, array[14] + spec.instance_variable_set :@has_rdoc, array[15] + spec.instance_variable_set :@loaded, false + + spec + end + + def warn_deprecated(old, new) + # How (if at all) to implement this? We only want to warn when + # a gem is being built, I should think. + end + + # REQUIRED gemspec attributes ------------------------------------ + + required_attribute :rubygems_version, RubyGemsVersion + required_attribute :specification_version, CURRENT_SPECIFICATION_VERSION + required_attribute :name + required_attribute :version + required_attribute :date, TODAY + required_attribute :summary + required_attribute :require_paths, ['lib'] + + # OPTIONAL gemspec attributes ------------------------------------ + + attributes :email, :homepage, :rubyforge_project, :description + attributes :autorequire, :default_executable + + attribute :bindir, 'bin' + attribute :has_rdoc, false + attribute :required_ruby_version, Gem::Requirement.default + attribute :required_rubygems_version, Gem::Requirement.default + attribute :platform, Gem::Platform::RUBY + + attribute :signing_key, nil + attribute :cert_chain, [] + attribute :post_install_message, nil + + array_attribute :authors + array_attribute :files + array_attribute :test_files + array_attribute :rdoc_options + array_attribute :extra_rdoc_files + array_attribute :executables + + # Array of extensions to build. See Gem::Installer#build_extensions for + # valid values. + + array_attribute :extensions + array_attribute :requirements + array_attribute :dependencies + + read_only :dependencies + + # ALIASED gemspec attributes ------------------------------------- + + attribute_alias_singular :executable, :executables + attribute_alias_singular :author, :authors + attribute_alias_singular :require_path, :require_paths + attribute_alias_singular :test_file, :test_files + + # DEPRECATED gemspec attributes ---------------------------------- + + def test_suite_file + warn_deprecated(:test_suite_file, :test_files) + test_files.first + end + + def test_suite_file=(val) + warn_deprecated(:test_suite_file, :test_files) + @test_files = [] unless defined? @test_files + @test_files << val + end + + # true when this gemspec has been loaded from a specifications directory. + # This attribute is not persisted. + + attr_writer :loaded + + # Path this gemspec was loaded from. This attribute is not persisted. + attr_accessor :loaded_from + + # Special accessor behaviours (overwriting default) -------------- + + overwrite_accessor :version= do |version| + @version = Version.create(version) + end + + overwrite_accessor :platform do + @new_platform + end + + overwrite_accessor :platform= do |platform| + @original_platform = platform if @original_platform.nil? + + case platform + when Gem::Platform::CURRENT then + @new_platform = Gem::Platform.local + + when Gem::Platform then + @new_platform = platform + + # legacy constants + when nil, Gem::Platform::RUBY then + @new_platform = Gem::Platform::RUBY + when Gem::Platform::WIN32 then + @new_platform = Gem::Platform::MSWIN32 + when Gem::Platform::LINUX_586 then + @new_platform = Gem::Platform::X86_LINUX + when Gem::Platform::DARWIN then + @new_platform = Gem::Platform::PPC_DARWIN + else + @new_platform = platform + end + + @platform = @new_platform.to_s + + @new_platform + end + + overwrite_accessor :required_ruby_version= do |value| + @required_ruby_version = Gem::Requirement.create(value) + end + + overwrite_accessor :required_rubygems_version= do |value| + @required_rubygems_version = Gem::Requirement.create(value) + end + + overwrite_accessor :date= do |date| + # We want to end up with a Time object with one-day resolution. + # This is the cleanest, most-readable, faster-than-using-Date + # way to do it. + case date + when String then + @date = Time.parse date + when Time then + @date = Time.parse date.strftime("%Y-%m-%d") + when Date then + @date = Time.parse date.to_s + else + @date = TODAY + end + end + + overwrite_accessor :date do + self.date = nil if @date.nil? # HACK Sets the default value for date + @date + end + + overwrite_accessor :summary= do |str| + @summary = if str then + str.strip. + gsub(/(\w-)\n[ \t]*(\w)/, '\1\2'). + gsub(/\n[ \t]*/, " ") + end + end + + overwrite_accessor :description= do |str| + @description = if str then + str.strip. + gsub(/(\w-)\n[ \t]*(\w)/, '\1\2'). + gsub(/\n[ \t]*/, " ") + end + end + + overwrite_accessor :default_executable do + begin + if defined? @default_executable and @default_executable + result = @default_executable + elsif @executables and @executables.size == 1 + result = Array(@executables).first + else + result = nil + end + result + rescue + nil + end + end + + def add_bindir(executables) + if not defined? @executables || @executables.nil? + return nil + end + + if defined? @bindir and @bindir then + Array(@executables).map {|e| File.join(@bindir, e) } + else + @executables + end + rescue + return nil + end + + overwrite_accessor :files do + result = [] + result.push(*@files) if defined?(@files) + result.push(*@test_files) if defined?(@test_files) + result.push(*(add_bindir(@executables))) + result.push(*@extra_rdoc_files) if defined?(@extra_rdoc_files) + result.push(*@extensions) if defined?(@extensions) + result.uniq.compact + end + + # Files in the Gem under one of the require_paths + def lib_files + @files.select do |file| + require_paths.any? do |path| + file.index(path) == 0 + end + end + end + + overwrite_accessor :test_files do + # Handle the possibility that we have @test_suite_file but not + # @test_files. This will happen when an old gem is loaded via + # YAML. + if defined? @test_suite_file then + @test_files = [@test_suite_file].flatten + @test_suite_file = nil + end + if defined? @test_files and @test_files then + @test_files + else + @test_files = [] + end + end + + # Predicates ----------------------------------------------------- + + def loaded?; @loaded ? true : false ; end + def has_rdoc?; has_rdoc ? true : false ; end + def has_unit_tests?; not test_files.empty?; end + alias has_test_suite? has_unit_tests? # (deprecated) + + # Constructors --------------------------------------------------- + + # Specification constructor. Assigns the default values to the + # attributes, adds this spec to the list of loaded specs (see + # Specification.list), and yields itself for further initialization. + # + def initialize + @new_platform = nil + assign_defaults + @loaded = false + @@list << self + + yield self if block_given? + + @@gather.call(self) if @@gather + end + + # Each attribute has a default value (possibly nil). Here, we + # initialize all attributes to their default value. This is + # done through the accessor methods, so special behaviours will + # be honored. Furthermore, we take a _copy_ of the default so + # each specification instance has its own empty arrays, etc. + def assign_defaults + @@nil_attributes.each do |name| + instance_variable_set name, nil + end + + @@non_nil_attributes.each do |name, default| + value = case default + when Time, Numeric, Symbol, true, false, nil then default + else default.dup + end + + instance_variable_set name, value + end + + # HACK + instance_variable_set :@new_platform, Gem::Platform::RUBY + end + + # Special loader for YAML files. When a Specification object is + # loaded from a YAML file, it bypasses the normal Ruby object + # initialization routine (#initialize). This method makes up for + # that and deals with gems of different ages. + # + # 'input' can be anything that YAML.load() accepts: String or IO. + # + def self.from_yaml(input) + input = normalize_yaml_input input + spec = YAML.load input + + if spec && spec.class == FalseClass then + raise Gem::EndOfYAMLException + end + + unless Gem::Specification === spec then + raise Gem::Exception, "YAML data doesn't evaluate to gem specification" + end + + unless (spec.instance_variables.include? '@specification_version' or + spec.instance_variables.include? :@specification_version) and + spec.instance_variable_get :@specification_version + spec.instance_variable_set :@specification_version, + NONEXISTENT_SPECIFICATION_VERSION + end + + spec + end + + def self.load(filename) + gemspec = nil + fail "NESTED Specification.load calls not allowed!" if @@gather + @@gather = proc { |gs| gemspec = gs } + data = File.read(filename) + eval(data) + gemspec + ensure + @@gather = nil + end + + # Make sure the yaml specification is properly formatted with dashes. + def self.normalize_yaml_input(input) + result = input.respond_to?(:read) ? input.read : input + result = "--- " + result unless result =~ /^--- / + result + end + + # Instance methods ----------------------------------------------- + + # Sets the rubygems_version to Gem::RubyGemsVersion. + # + def mark_version + @rubygems_version = RubyGemsVersion + end + + # Ignore unknown attributes if the + def method_missing(sym, *a, &b) # :nodoc: + if @specification_version > CURRENT_SPECIFICATION_VERSION and + sym.to_s =~ /=$/ then + warn "ignoring #{sym} loading #{full_name}" if $DEBUG + else + super + end + end + + # Adds a dependency to this Gem. For example, + # + # spec.add_dependency('jabber4r', '> 0.1', '<= 0.5') + # + # gem:: [String or Gem::Dependency] The Gem name/dependency. + # requirements:: [default=">= 0"] The version requirements. + # + def add_dependency(gem, *requirements) + requirements = if requirements.empty? then + Gem::Requirement.default + else + requirements.flatten + end + + unless gem.respond_to?(:name) && gem.respond_to?(:version_requirements) + gem = Dependency.new(gem, requirements) + end + + dependencies << gem + 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}" + else + "#{@name}-#{@version}-#{platform}" + end + end + + # The full path to the gem (install path + full name). + # + # return:: [String] the full gem path + # + def full_gem_path + path = File.join installation_path, 'gems', full_name + return path if File.directory? path + File.join installation_path, 'gems', + "#{name}-#{version}-#{@original_platform}" + end + + # The default (generated) file name of the gem. + def file_name + full_name + ".gem" + end + + # The root directory that the gem was installed into. + # + # return:: [String] the installation path + # + def installation_path + (File.dirname(@loaded_from).split(File::SEPARATOR)[0..-2]). + join(File::SEPARATOR) + end + + # Checks if this Specification meets the requirement of the supplied + # dependency. + # + # dependency:: [Gem::Dependency] the dependency to check + # return:: [Boolean] true if dependency is met, otherwise false + # + def satisfies_requirement?(dependency) + return @name == dependency.name && + dependency.version_requirements.satisfied_by?(@version) + end + + # Comparison methods --------------------------------------------- + + def sort_obj + [@name, @version.to_ints, @new_platform == Gem::Platform::RUBY ? -1 : 1] + end + + def <=>(other) # :nodoc: + sort_obj <=> other.sort_obj + end + + # Tests specs for equality (across all attributes). + def ==(other) # :nodoc: + self.class === other && same_attributes?(other) + end + + alias eql? == # :nodoc: + + def same_attributes?(other) + @@attributes.each do |name, default| + return false unless self.send(name) == other.send(name) + end + true + end + private :same_attributes? + + def hash # :nodoc: + @@attributes.inject(0) { |hash_code, (name, default_value)| + n = self.send(name).hash + hash_code + n + } + end + + # Export methods (YAML and Ruby code) ---------------------------- + + # Returns an array of attribute names to be used when generating a + # YAML representation of this object. If an attribute still has + # its default value, it is omitted. + def to_yaml_properties + mark_version + @@attributes.map { |name, default| "@#{name}" } + end + + def yaml_initialize(tag, vals) + vals.each do |ivar, val| + instance_variable_set "@#{ivar}", val + end + + @original_platform = @platform # for backwards compatibility + self.platform = Gem::Platform.new @platform + end + + # Returns a Ruby code representation of this specification, such that it + # can be eval'ed and reconstruct the same specification later. Attributes + # that still have their default values are omitted. + def to_ruby + mark_version + result = [] + result << "Gem::Specification.new do |s|" + + result << " s.name = #{ruby_code name}" + result << " s.version = #{ruby_code version}" + result << "" + result << " s.specification_version = #{specification_version} if s.respond_to? :specification_version=" + result << "" + result << " s.required_rubygems_version = #{ruby_code required_rubygems_version} if s.respond_to? :required_rubygems_version=" + + handled = [ + :dependencies, + :name, + :required_rubygems_version, + :specification_version, + :version, + ] + + attributes = @@attributes.sort_by { |name,| name.to_s } + + attributes.each do |name, default| + next if handled.include? name + current_value = self.send(name) + if current_value != default or self.class.required_attribute? name then + result << " s.#{name} = #{ruby_code current_value}" + end + end + + result << "" unless dependencies.empty? + + dependencies.each do |dep| + version_reqs_param = dep.requirements_list.inspect + result << " s.add_dependency(%q<#{dep.name}>, #{version_reqs_param})" + end + + result << "end" + result << "" + + result.join "\n" + end + + # Validation and normalization methods --------------------------- + + # Checks that the specification contains all required fields, and + # does a very basic sanity check. + # + # Raises InvalidSpecificationException if the spec does not pass + # the checks.. + def validate + normalize + + if rubygems_version != RubyGemsVersion then + raise Gem::InvalidSpecificationException, + "expected RubyGems version #{RubyGemsVersion}, was #{rubygems_version}" + end + + @@required_attributes.each do |symbol| + unless self.send symbol then + raise Gem::InvalidSpecificationException, + "missing value for attribute #{symbol}" + end + end + + if require_paths.empty? then + raise Gem::InvalidSpecificationException, + "specification must have at least one require_path" + end + + case platform + when Gem::Platform, Platform::RUBY then # ok + else + raise Gem::InvalidSpecificationException, + "invalid platform #{platform.inspect}, see Gem::Platform" + end + + true + end + + # Normalize the list of files so that: + # * All file lists have redundancies removed. + # * Files referenced in the extra_rdoc_files are included in the + # package file list. + # + # Also, the summary and description are converted to a normal + # format. + def normalize + if defined? @extra_rdoc_files and @extra_rdoc_files then + @extra_rdoc_files.uniq! + @files ||= [] + @files.concat(@extra_rdoc_files) + end + @files.uniq! if @files + end + + # Dependency methods --------------------------------------------- + + # Return a list of all gems that have a dependency on this + # gemspec. The list is structured with entries that conform to: + # + # [depending_gem, dependency, [list_of_gems_that_satisfy_dependency]] + # + # return:: [Array] [[dependent_gem, dependency, [list_of_satisfiers]]] + # + def dependent_gems + out = [] + Gem.source_index.each do |name,gem| + gem.dependencies.each do |dep| + if self.satisfies_requirement?(dep) then + sats = [] + find_all_satisfiers(dep) do |sat| + sats << sat + end + out << [gem, dep, sats] + end + end + end + out + end + + def to_s + "#" + end + + private + + def find_all_satisfiers(dep) + Gem.source_index.each do |name,gem| + if(gem.satisfies_requirement?(dep)) then + yield gem + end + end + end + + # Return a string containing a Ruby code representation of the + # given object. + def ruby_code(obj) + case obj + when String then '%q{' + obj + '}' + when Array then obj.inspect + when Gem::Version then obj.to_s.inspect + when Date then '%q{' + obj.strftime('%Y-%m-%d') + '}' + when Time then '%q{' + obj.strftime('%Y-%m-%d') + '}' + when Numeric then obj.inspect + when true, false, nil then obj.inspect + when Gem::Platform then "Gem::Platform.new(#{obj.to_a.inspect})" + when Gem::Requirement then "Gem::Requirement.new(#{obj.to_s.inspect})" + else raise Exception, "ruby_code case not handled: #{obj.class}" + end + end + + end + +end + diff --git a/lib/rubygems/timer.rb b/lib/rubygems/timer.rb new file mode 100755 index 0000000000..06250f26b5 --- /dev/null +++ b/lib/rubygems/timer.rb @@ -0,0 +1,25 @@ +# +# This file defines a $log variable for logging, and a time() method for recording timing +# information. +# +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + + +$log = Object.new +def $log.debug(str) + STDERR.puts str +end + +def time(msg, width=25) + t = Time.now + return_value = yield + elapsed = Time.now.to_f - t.to_f + elapsed = sprintf("%3.3f", elapsed) + $log.debug "#{msg.ljust(width)}: #{elapsed}s" + return_value +end + diff --git a/lib/rubygems/uninstaller.rb b/lib/rubygems/uninstaller.rb new file mode 100644 index 0000000000..0f7edb048c --- /dev/null +++ b/lib/rubygems/uninstaller.rb @@ -0,0 +1,183 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'fileutils' +require 'rubygems' +require 'rubygems/dependency_list' +require 'rubygems/doc_manager' +require 'rubygems/user_interaction' + +## +# An Uninstaller. +# +class Gem::Uninstaller + + include Gem::UserInteraction + + ## + # Constructs an Uninstaller instance + # + # gem:: [String] The Gem name to uninstall + # + def initialize(gem, options) + @gem = gem + @version = options[:version] || Gem::Requirement.default + @force_executables = options[:executables] + @force_all = options[:all] + @force_ignore = options[:ignore] + end + + ## + # Performs the uninstall of the Gem. This removes the spec, the + # Gem directory, and the cached .gem file, + # + def uninstall + list = Gem.source_index.search(/^#{@gem}$/, @version) + + if list.empty? then + raise Gem::InstallError, "Unknown gem #{@gem}-#{@version}" + elsif list.size > 1 && @force_all + remove_all(list.dup) + remove_executables(list.last) + elsif list.size > 1 + say + gem_names = list.collect {|gem| gem.full_name} + ["All versions"] + gem_name, index = + choose_from_list("Select gem to uninstall:", gem_names) + if index == list.size + remove_all(list.dup) + remove_executables(list.last) + elsif index >= 0 && index < list.size + to_remove = list[index] + remove(to_remove, list) + remove_executables(to_remove) + else + say "Error: must enter a number [1-#{list.size+1}]" + end + else + remove(list[0], list.dup) + remove_executables(list.last) + end + end + + ## + # Remove executables and batch files (windows only) for the gem as + # it is being installed + # + # gemspec::[Specification] the gem whose executables need to be removed. + # + def remove_executables(gemspec) + return if gemspec.nil? + if(gemspec.executables.size > 0) + raise Gem::FilePermissionError.new(Gem.bindir) unless + File.writable?(Gem.bindir) + list = Gem.source_index.search(gemspec.name).delete_if { |spec| + spec.version == gemspec.version + } + executables = gemspec.executables.clone + list.each do |spec| + spec.executables.each do |exe_name| + executables.delete(exe_name) + end + end + return if executables.size == 0 + answer = @force_executables || ask_yes_no( + "Remove executables and scripts for\n" + + "'#{gemspec.executables.join(", ")}' in addition to the gem?", + true) # " # appease ruby-mode - don't ask + unless answer + say "Executables and scripts will remain installed." + return + else + gemspec.executables.each do |exe_name| + say "Removing #{exe_name}" + File.unlink File.join(Gem.bindir, exe_name) rescue nil + File.unlink File.join(Gem.bindir, exe_name + ".bat") rescue nil + end + end + end + end + + # + # list:: the list of all gems to remove + # + # Warning: this method modifies the +list+ parameter. Once it has + # uninstalled a gem, it is removed from that list. + # + def remove_all(list) + list.dup.each { |gem| remove(gem, list) } + end + + # + # spec:: the spec of the gem to be uninstalled + # list:: the list of all such gems + # + # Warning: this method modifies the +list+ parameter. Once it has + # uninstalled a gem, it is removed from that list. + # + def remove(spec, list) + unless ok_to_remove? spec then + raise Gem::DependencyRemovalException, + "Uninstallation aborted due to dependent gem(s)" + end + + raise Gem::FilePermissionError, spec.installation_path unless + File.writable?(spec.installation_path) + + FileUtils.rm_rf spec.full_gem_path + + original_platform_name = [ + spec.name, spec.version, spec.original_platform].join '-' + + spec_dir = File.join spec.installation_path, 'specifications' + gemspec = File.join spec_dir, "#{spec.full_name}.gemspec" + + unless File.exist? gemspec then + gemspec = File.join spec_dir, "#{original_platform_name}.gemspec" + end + + FileUtils.rm_rf gemspec + + cache_dir = File.join spec.installation_path, 'cache' + gem = File.join cache_dir, "#{spec.full_name}.gem" + + unless File.exist? gemspec then + gem = File.join cache_dir, "#{original_platform_name}.gem" + end + + FileUtils.rm_rf gem + + Gem::DocManager.new(spec).uninstall_doc + + say "Successfully uninstalled #{spec.full_name}" + + list.delete spec + end + + def ok_to_remove?(spec) + return true if @force_ignore + + srcindex = Gem::SourceIndex.from_installed_gems + deplist = Gem::DependencyList.from_source_index srcindex + deplist.ok_to_remove?(spec.full_name) || ask_if_ok(spec) + end + + def ask_if_ok(spec) + msg = [''] + msg << 'You have requested to uninstall the gem:' + msg << "\t#{spec.full_name}" + spec.dependent_gems.each do |gem,dep,satlist| + msg << + ("#{gem.name}-#{gem.version} depends on " + + "[#{dep.name} (#{dep.version_requirements})]") + end + msg << 'If you remove this gems, one or more dependencies will not be met.' + msg << 'Continue with Uninstall?' + return ask_yes_no(msg.join("\n"), true) + end + +end + diff --git a/lib/rubygems/user_interaction.rb b/lib/rubygems/user_interaction.rb new file mode 100644 index 0000000000..7ff03eaadf --- /dev/null +++ b/lib/rubygems/user_interaction.rb @@ -0,0 +1,291 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +module Gem + + #################################################################### + # Module that defines the default UserInteraction. Any class + # including this module will have access to the +ui+ method that + # returns the default UI. + module DefaultUserInteraction + + # Return the default UI. + def ui + DefaultUserInteraction.ui + end + + # Set the default UI. If the default UI is never explicity set, a + # simple console based UserInteraction will be used automatically. + def ui=(new_ui) + DefaultUserInteraction.ui = new_ui + end + + def use_ui(new_ui, &block) + DefaultUserInteraction.use_ui(new_ui, &block) + end + + # The default UI is a class variable of the singleton class for + # this module. + + @ui = nil + + class << self + def ui + @ui ||= Gem::ConsoleUI.new + end + def ui=(new_ui) + @ui = new_ui + end + def use_ui(new_ui) + old_ui = @ui + @ui = new_ui + yield + ensure + @ui = old_ui + end + end + end + + #################################################################### + # Make the default UI accessable without the "ui." prefix. Classes + # including this module may use the interaction methods on the + # default UI directly. Classes may also reference the +ui+ and + # ui= methods. + # + # Example: + # + # class X + # include Gem::UserInteraction + # + # def get_answer + # n = ask("What is the meaning of life?") + # end + # end + module UserInteraction + include DefaultUserInteraction + [ + :choose_from_list, :ask, :ask_yes_no, :say, :alert, :alert_warning, + :alert_error, :terminate_interaction!, :terminate_interaction + ].each do |methname| + class_eval %{ + def #{methname}(*args) + ui.#{methname}(*args) + end + } + end + end + + #################################################################### + # StreamUI implements a simple stream based user interface. + class StreamUI + + attr_reader :ins, :outs, :errs + + def initialize(in_stream, out_stream, err_stream=STDERR) + @ins = in_stream + @outs = out_stream + @errs = err_stream + end + + # Choose from a list of options. +question+ is a prompt displayed + # above the list. +list+ is a list of option strings. Returns + # the pair [option_name, option_index]. + def choose_from_list(question, list) + @outs.puts question + list.each_with_index do |item, index| + @outs.puts " #{index+1}. #{item}" + end + @outs.print "> " + @outs.flush + + result = @ins.gets + + return nil, nil unless result + + result = result.strip.to_i - 1 + return list[result], result + end + + # Ask a question. Returns a true for yes, false for no. If not + # connected to a tty, raises an exception if default is nil, + # otherwise returns default. + def ask_yes_no(question, default=nil) + if not @ins.tty? then + if default.nil? then + raise( + Gem::OperationNotSupportedError, + "Not connected to a tty and no default specified") + else + return default + end + end + qstr = case default + when nil + 'yn' + when true + 'Yn' + else + 'yN' + end + result = nil + while result.nil? + result = ask("#{question} [#{qstr}]") + result = case result + when /^[Yy].*/ + true + when /^[Nn].*/ + false + when /^$/ + default + else + nil + end + end + return result + end + + # Ask a question. Returns an answer if connected to a tty, nil + # otherwise. + def ask(question) + return nil if not @ins.tty? + @outs.print(question + " ") + @outs.flush + result = @ins.gets + result.chomp! if result + result + end + + # Display a statement. + def say(statement="") + @outs.puts statement + end + + # Display an informational alert. + def alert(statement, question=nil) + @outs.puts "INFO: #{statement}" + return ask(question) if question + end + + # Display a warning in a location expected to get error messages. + def alert_warning(statement, question=nil) + @errs.puts "WARNING: #{statement}" + ask(question) if question + end + + # Display an error message in a location expected to get error + # messages. + def alert_error(statement, question=nil) + @errs.puts "ERROR: #{statement}" + ask(question) if question + end + + # Terminate the application immediately without running any exit + # handlers. + def terminate_interaction!(status=-1) + exit!(status) + end + + # Terminate the appliation normally, running any exit handlers + # that might have been defined. + def terminate_interaction(status=0) + exit(status) + end + + # Return a progress reporter object + def progress_reporter(*args) + case Gem.configuration.verbose + when nil, false + SilentProgressReporter.new(@outs, *args) + when true + SimpleProgressReporter.new(@outs, *args) + else + VerboseProgressReporter.new(@outs, *args) + end + end + + class SilentProgressReporter + attr_reader :count + + def initialize(out_stream, size, initial_message, terminal_message = nil) + end + + def updated(message) + end + + def done + end + end + + class SimpleProgressReporter + include DefaultUserInteraction + + attr_reader :count + + def initialize(out_stream, size, initial_message, + terminal_message = "complete") + @out = out_stream + @total = size + @count = 0 + @terminal_message = terminal_message + + @out.puts initial_message + end + + def updated(message) + @count += 1 + @out.print "." + @out.flush + end + + def done + @out.puts "\n#{@terminal_message}" + end + end + + class VerboseProgressReporter + include DefaultUserInteraction + + attr_reader :count + + def initialize(out_stream, size, initial_message, + terminal_message = 'complete') + @out = out_stream + @total = size + @count = 0 + @terminal_message = terminal_message + + @out.puts initial_message + end + + def updated(message) + @count += 1 + @out.puts "#{@count}/#{@total}: #{message}" + end + + def done + @out.puts @terminal_message + end + end + end + + #################################################################### + # Subclass of StreamUI that instantiates the user interaction using + # standard in, out and error. + class ConsoleUI < StreamUI + def initialize + super(STDIN, STDOUT, STDERR) + end + end + + #################################################################### + # SilentUI is a UI choice that is absolutely silent. + class SilentUI + def method_missing(sym, *args, &block) + self + end + end +end + diff --git a/lib/rubygems/validator.rb b/lib/rubygems/validator.rb new file mode 100755 index 0000000000..8130f49bc8 --- /dev/null +++ b/lib/rubygems/validator.rb @@ -0,0 +1,185 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'find' + +require 'rubygems/digest/md5' +require 'rubygems/format' +require 'rubygems/installer' + +module Gem + + ## + # Validator performs various gem file and gem database validation + class Validator + include UserInteraction + + ## + # Given a gem file's contents, validates against its own MD5 checksum + # gem_data:: [String] Contents of the gem file + def verify_gem(gem_data) + raise VerificationError, 'empty gem file' if gem_data.size == 0 + + unless gem_data =~ /MD5SUM/ then + return # Don't worry about it...this sucks. Need to fix MD5 stuff for + # new format + # FIXME + end + + sum_data = gem_data.gsub(/MD5SUM = "([a-z0-9]+)"/, + "MD5SUM = \"#{"F" * 32}\"") + + unless Gem::MD5.hexdigest(sum_data) == $1.to_s then + raise VerificationError, 'invalid checksum for gem file' + end + end + + ## + # Given the path to a gem file, validates against its own MD5 checksum + # + # gem_path:: [String] Path to gem file + def verify_gem_file(gem_path) + File.open gem_path, 'rb' do |file| + gem_data = file.read + verify_gem gem_data + end + rescue Errno::ENOENT + raise Gem::VerificationError.new("missing gem file #{gem_path}") + end + + private + def find_files_for_gem(gem_directory) + installed_files = [] + Find.find(gem_directory) {|file_name| + fn = file_name.slice((gem_directory.size)..(file_name.size-1)).sub(/^\//, "") + if(!(fn =~ /CVS/ || File.directory?(fn) || fn == "")) then + installed_files << fn + end + + } + installed_files + end + + + public + ErrorData = Struct.new(:path, :problem) + + ## + # Checks the gem directory for the following potential + # inconsistencies/problems: + # * Checksum gem itself + # * For each file in each gem, check consistency of installed versions + # * Check for files that aren't part of the gem but are in the gems directory + # * 1 cache - 1 spec - 1 directory. + # + # returns a hash of ErrorData objects, keyed on the problem gem's name. + def alien + errors = {} + Gem::SourceIndex.from_installed_gems.each do |gem_name, gem_spec| + errors[gem_name] ||= [] + gem_path = File.join(Gem.dir, "cache", gem_spec.full_name) + ".gem" + spec_path = File.join(Gem.dir, "specifications", gem_spec.full_name) + ".gemspec" + gem_directory = File.join(Gem.dir, "gems", gem_spec.full_name) + installed_files = find_files_for_gem(gem_directory) + + if(!File.exist?(spec_path)) then + errors[gem_name] << ErrorData.new(spec_path, "Spec file doesn't exist for installed gem") + end + + begin + verify_gem_file(gem_path) + File.open(gem_path, 'rb') do |file| + format = Gem::Format.from_file_by_path(gem_path) + format.file_entries.each do |entry, data| + # Found this file. Delete it from list + installed_files.delete remove_leading_dot_dir(entry['path']) + + next unless data # HACK `gem check -a mkrf` + + File.open(File.join(gem_directory, entry['path']), 'rb') do |f| + unless Gem::MD5.hexdigest(f.read).to_s == + Gem::MD5.hexdigest(data).to_s then + errors[gem_name] << ErrorData.new(entry['path'], "installed file doesn't match original from gem") + end + end + end + end + rescue VerificationError => e + errors[gem_name] << ErrorData.new(gem_path, e.message) + end + # Clean out directories that weren't explicitly included in the gemspec + # FIXME: This still allows arbitrary incorrect directories. + installed_files.delete_if {|potential_directory| + File.directory?(File.join(gem_directory, potential_directory)) + } + if(installed_files.size > 0) then + errors[gem_name] << ErrorData.new(gem_path, "Unmanaged files in gem: #{installed_files.inspect}") + end + end + errors + end + + class TestRunner + def initialize(suite, ui) + @suite = suite + @ui = ui + end + + def self.run(suite, ui) + require 'test/unit/ui/testrunnermediator' + return new(suite, ui).start + end + + def start + @mediator = Test::Unit::UI::TestRunnerMediator.new(@suite) + @mediator.add_listener(Test::Unit::TestResult::FAULT, &method(:add_fault)) + return @mediator.run_suite + end + + def add_fault(fault) + if Gem.configuration.verbose then + @ui.say fault.long_display + end + end + end + + autoload :TestRunner, 'test/unit/ui/testrunnerutilities' + + ## + # Runs unit tests for a given gem specification + def unit_test(gem_spec) + start_dir = Dir.pwd + Dir.chdir(gem_spec.full_gem_path) + $: << File.join(Gem.dir, "gems", gem_spec.full_name) + # XXX: why do we need this gem_spec when we've already got 'spec'? + test_files = gem_spec.test_files + if test_files.empty? + say "There are no unit tests to run for #{gem_spec.name}-#{gem_spec.version}" + return + end + gem gem_spec.name, "= #{gem_spec.version.version}" + test_files.each do |f| require f end + suite = Test::Unit::TestSuite.new("#{gem_spec.name}-#{gem_spec.version}") + ObjectSpace.each_object(Class) do |klass| + suite << klass.suite if (klass < Test::Unit::TestCase) + end + result = TestRunner.run(suite, ui()) + unless result.passed? + alert_error(result.to_s) + #unless ask_yes_no(result.to_s + "...keep Gem?", true) then + #Gem::Uninstaller.new(gem_spec.name, gem_spec.version.version).uninstall + #end + end + result + ensure + Dir.chdir(start_dir) + end + + def remove_leading_dot_dir(path) + path.sub(/^\.\//, "") + end + end +end diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb new file mode 100644 index 0000000000..35dd60a74a --- /dev/null +++ b/lib/rubygems/version.rb @@ -0,0 +1,158 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems' + +## +# The Version class processes string versions into comparable values +class Gem::Version + + include Comparable + + attr_reader :ints + + attr_reader :version + + ## + # Checks if version string is valid format + # + # str:: [String] the version string + # return:: [Boolean] true if the string format is correct, otherwise false + # + def self.correct?(version) + case version + when Integer, /\A\s*(\d+(\.\d+)*)*\s*\z/ then true + else false + end + end + + ## + # Factory method to create a Version object. Input may be a Version or a + # String. Intended to simplify client code. + # + # ver1 = Version.create('1.3.17') # -> (Version object) + # ver2 = Version.create(ver1) # -> (ver1) + # ver3 = Version.create(nil) # -> nil + # + def self.create(input) + if input.respond_to? :version then + input + elsif input.nil? then + nil + else + new input + end + end + + ## + # Constructs a version from the supplied string + # + # version:: [String] The version string. Format is digit.digit... + # + def initialize(version) + raise ArgumentError, "Malformed version number string #{version}" unless + self.class.correct?(version) + + self.version = version + end + + def inspect # :nodoc: + "#<#{self.class} #{@version.inspect}>" + end + + # Dump only the raw version string, not the complete object + def marshal_dump + [@version] + end + + # Load custom marshal format + def marshal_load(array) + self.version = array[0] + end + + # Strip ignored trailing zeros. + def normalize + @ints = @version.to_s.scan(/\d+/).map { |s| s.to_i } + + return if @ints.length == 1 + + @ints.pop while @ints.last == 0 + + @ints = [0] if @ints.empty? + end + + ## + # Returns the text representation of the version + # + # return:: [String] version as string + # + def to_s + @version + end + + ## + # Convert version to integer array + # + # return:: [Array] list of integers + # + def to_ints + normalize unless @ints + @ints + end + + def to_yaml_properties + ['@version'] + end + + def version=(version) + @version = version.to_s.strip + normalize + end + + def yaml_initialize(tag, values) + self.version = values['version'] + end + + ## + # Compares two versions + # + # other:: [Version or .ints] other version to compare to + # return:: [Fixnum] -1, 0, 1 + # + def <=>(other) + return 1 unless other + @ints <=> other.ints + end + + def hash + to_ints.inject { |hash_code, n| hash_code + n } + end + + # Return a new version object where the next to the last revision + # number is one greater. (e.g. 5.3.1 => 5.4) + def bump + ints = @ints.dup + ints.pop if ints.size > 1 + ints[-1] += 1 + self.class.new(ints.join(".")) + end + + #:stopdoc: + + require 'rubygems/requirement' + + # Gem::Requirement's original definition is nested in Version. + # Although an inappropriate place, current gems specs reference the nested + # class name explicitly. To remain compatible with old software loading + # gemspecs, we leave a copy of original definition in Version, but define an + # alias Gem::Requirement for use everywhere else. + + Requirement = ::Gem::Requirement + + # :startdoc: + +end + diff --git a/lib/rubygems/version_option.rb b/lib/rubygems/version_option.rb new file mode 100644 index 0000000000..54f85188df --- /dev/null +++ b/lib/rubygems/version_option.rb @@ -0,0 +1,49 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems' + +# Mixin methods for --version and --platform Gem::Command options. +module Gem::VersionOption + + # Add the --platform option to the option parser. + def add_platform_option(task = command, *wrap) + OptionParser.accept Gem::Platform do |value| + if value == Gem::Platform::RUBY then + value + else + Gem::Platform.new value + end + end + + add_option('--platform PLATFORM', Gem::Platform, + "Specify the platform of gem to #{task}", *wrap) do + |value, options| + unless options[:added_platform] then + Gem.platforms.clear + Gem.platforms << Gem::Platform::RUBY + options[:added_platform] = true + end + + Gem.platforms << value unless Gem.platforms.include? value + end + end + + # Add the --version option to the option parser. + def add_version_option(task = command, *wrap) + OptionParser.accept Gem::Requirement do |value| + Gem::Requirement.new value + end + + add_option('-v', '--version VERSION', Gem::Requirement, + "Specify version of gem to #{task}", *wrap) do + |value, options| + options[:version] = value + end + end + +end + diff --git a/lib/ubygems.rb b/lib/ubygems.rb new file mode 100644 index 0000000000..fec880f73b --- /dev/null +++ b/lib/ubygems.rb @@ -0,0 +1,10 @@ +# This file allows for the running of rubygems with a nice +# command line look-and-feel: ruby -rubygems foo.rb +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + + +require 'rubygems' diff --git a/test/rubygems/bogussources.rb b/test/rubygems/bogussources.rb new file mode 100644 index 0000000000..008e3a1de5 --- /dev/null +++ b/test/rubygems/bogussources.rb @@ -0,0 +1,8 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems' +Gem.use_paths("test/mock/gems") diff --git a/test/rubygems/data/gem-private_key.pem b/test/rubygems/data/gem-private_key.pem new file mode 100644 index 0000000000..3e4be4cd9a --- /dev/null +++ b/test/rubygems/data/gem-private_key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAz0tTOtsJuHDKAEXrQx0f6DUEzBEUTSLR1fk0iEHsY9rDCQxm +sw5Bf2UnVhdD03B4/XzIK+pat2CMQc37/vLIBuVgS7g/fzatGiM0m5rAHtycr0XU +8Ek6zjx4iSv70OLjybY+/utHCEc838awGDMCFR21jYxgATPVwqAIyasvwbKh/Vhw +uErFPqT9G8BKTHsaX+H+ADIRH001OmWkjB6EyjF05114kNMa0+2C7daV9hoBL3md +hCt6zOGcapl/9LkGxhcNEUB/So16V1ZQldg9macGyWktyNTSfctlF+f8okAmicG3 +XIwaW8UTmjFCmvDs/h1R/uKpe2IOHz87n29d2QIDAQABAoIBAQCR6n/nyg+JmTtX +/d+hGns/RTLfQpZ7xarXZ9gmoeD4WSE42VXhbIOGXXnXDAFecKl6Jb/xycGZm4if +OZPM3rEWyZeDNWrc7WvkHiwF7GSYVMqmRg2iJqoSSla+mAtl+pBFiNfHMW6K0Tp0 +erOyFRW+L2+A9/MMZaRun6AP9URkn0jz2kwmMFf+6szmzVn6fPFzZDRI+hEeaDmi +LBzSrfrddrIBX+xGEoBj6RmfnKBCSUVSSxOauYjd4mVjVYxvMH4SV1hXDUS5GPl5 +MbCiBb7bpNIg/8ljMoRrQiqk0XwwS7MaCqPtMhUtpSmC/zSjAfmoN7AOc/Xh69cQ +OCMNZH9BAoGBAPBlsuuU6fg0gVTKDdR12jHx03uRRt8/nPxHnpJkZCIh9XKh1LtY +bkumi9HZpp3mzDiaGg/rwfCwNckKx8NLhICLgkric6ClrKftxTu6C8tBAb5YDi6u +74KYnV8lMY/unzBtIloPgM3uluS292POmrWZpKwhvHLD71MewzMor5HFAoGBANy/ +mwsBs8i3Gzk8Twjq8effhPpE7kpxhC7bhwmjX3q41EjQWDT8M6xb1P9dRSsCIebi +kqP1yhl27dJpA8r5WqE/z89xhBvObAGRv41eXxOI0LaH2k5lJQrUeSC+51dy+BEB +T3GXD4C5ezZHQ8Wz/oL73uikrfhD+AqOZT2YbMEFAoGBAJvWEWpOGm3f+4bvhI+Z +5lxCG4oa3wqRvj58XvsfQRovUWGCLtlTtgwsZq8enLf3iaOXohV4Czzvva4Z4u1i +4v5BcbEBo1scixRBOn5BWKvl9C9j/a2dkX3jWQD4p2xaj69gz8f6DNFyPTb+tNhq +cjgO5YUASZ1MDrSfWIKteULRAoGAZkZv8x2KyofrmQ0UITGZerDYz4t4TA1kDMGx +QwnqhtVzpXjCJWpkFotFmDsCfPaz9mErR8PtKvcrIL1/AF+fWe5Sve3+I1P0PpXk +hf8fVdGhwbAXuRKrouTmagGI9b9Sp65PvHUcvasyJufFwqeuV8mScX87CzeSiHGI +/ozMdnECgYEAq4+losrhe0DEmiC9zVPvwRXjbSixDsSJxHfOcqIsZqhUgBiZ4TJD +SrkuukrMZib6BAD+PtCJS1TBbJyyvL3QecizhHSIh3ZnT0HnaRPatLEYmU65+3kE +kTqL4ik92bJnnWowy677sydl1lzBJDVa9ZlTs7BFSd8y/0DZaUxGg2I= +-----END RSA PRIVATE KEY----- diff --git a/test/rubygems/data/gem-public_cert.pem b/test/rubygems/data/gem-public_cert.pem new file mode 100644 index 0000000000..885bf7f369 --- /dev/null +++ b/test/rubygems/data/gem-public_cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDNjCCAh6gAwIBAgIBADANBgkqhkiG9w0BAQUFADBBMREwDwYDVQQDDAhydWJ5 +Z2VtczEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUxEzARBgoJkiaJk/IsZAEZFgNj +b20wHhcNMDcwODAyMDMyNTQyWhcNMDgwODAxMDMyNTQyWjBBMREwDwYDVQQDDAhy +dWJ5Z2VtczEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUxEzARBgoJkiaJk/IsZAEZ +FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPS1M62wm4cMoA +RetDHR/oNQTMERRNItHV+TSIQexj2sMJDGazDkF/ZSdWF0PTcHj9fMgr6lq3YIxB +zfv+8sgG5WBLuD9/Nq0aIzSbmsAe3JyvRdTwSTrOPHiJK/vQ4uPJtj7+60cIRzzf +xrAYMwIVHbWNjGABM9XCoAjJqy/BsqH9WHC4SsU+pP0bwEpMexpf4f4AMhEfTTU6 +ZaSMHoTKMXTnXXiQ0xrT7YLt1pX2GgEveZ2EK3rM4ZxqmX/0uQbGFw0RQH9KjXpX +VlCV2D2ZpwbJaS3I1NJ9y2UX5/yiQCaJwbdcjBpbxROaMUKa8Oz+HVH+4ql7Yg4f +Pzufb13ZAgMBAAGjOTA3MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW +BBRYTAoj4cn8CWZMHFnHGQgoO5jyFTANBgkqhkiG9w0BAQUFAAOCAQEATRrJC05l +dOmx67Sy3bU+AVXkOr7B9nn2Myqo9uSIAncPoElN6aHr/Q8wOOjtok4r0JcHPe1e +eotDCZUE1Jkl13Tpv26rOfOOUHtGlyAIAtpsUGOraaJkSut4WKLr1/KckyAAEtgP +c13A0s0mEiWFRuYxIdEi54561pTT2qQBE/DUPGoYD5rUg9XYAlSovMMwG99Oca7L +cI6vCymr1bzzddExoywBNOy0fbBT62I3ICBGbH5yOVVKVmlxeo2Zp10FCj0kDrnq +OuMJSDr5I2XPYqoC+W4YSbwn55o2jGIUX1lOq2Hvj4tFgSxlnJZn0tUhBfR3gSOn +IFnrqu8PlZsLFw== +-----END CERTIFICATE----- diff --git a/test/rubygems/fake_certlib/openssl.rb b/test/rubygems/fake_certlib/openssl.rb new file mode 100644 index 0000000000..948110f078 --- /dev/null +++ b/test/rubygems/fake_certlib/openssl.rb @@ -0,0 +1,7 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +fail LoadError, "no such file to load -- openssl" diff --git a/test/rubygems/functional.rb b/test/rubygems/functional.rb new file mode 100644 index 0000000000..4838167324 --- /dev/null +++ b/test/rubygems/functional.rb @@ -0,0 +1,95 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require 'rubygems' +require 'test/insure_session' +require 'rubygems/format' +require 'rubygems/command_manager' + +class FunctionalTest < Test::Unit::TestCase + def setup + @gem_path = File.expand_path("bin/gem") + lib_path = File.expand_path("lib") + @ruby_options = "-I#{lib_path} -I." + @verbose = false + end + + def test_gem_help_options + gem_nossl 'help options' + assert_match(/Usage:/, @out, @err) + assert_status + end + + def test_gem_help_commands + gem_nossl 'help commands' + assert_match(/gem install/, @out) + assert_status + end + + def test_gem_no_args_shows_help + gem_nossl + assert_match(/Usage:/, @out) + assert_status 1 + end + + # This test is disabled because of the insanely long time it takes + # to time out. + def xtest_bogus_source_hoses_up_remote_install_but_gem_command_gives_decent_error_message + @ruby_options << " -rtest/bogussources" + gem_nossl "install asdf --remote" + assert_match(/error/im, @err) + assert_status 1 + end + + def test_all_command_helps + mgr = Gem::CommandManager.new + mgr.command_names.each do |cmdname| + gem_nossl "help #{cmdname}" + assert_match(/Usage: gem #{cmdname}/, @out, + "should see help for #{cmdname}") + end + end + + # :section: Help Methods + + # Run a gem command without the SSL library. + def gem_nossl(options="") + old_options = @ruby_options.dup + @ruby_options << " -Itest/fake_certlib" + gem(options) + ensure + @ruby_options = old_options + end + + # Run a gem command with the SSL library. + def gem_withssl(options="") + gem(options) + end + + # Run a gem command for the functional test. + def gem(options="") + shell = Session::Shell.new + options = options + " --config-file missing_file" if options !~ /--config-file/ + command = "#{Gem.ruby} #{@ruby_options} #{@gem_path} #{options}" + puts "\n\nCOMMAND: [#{command}]" if @verbose + @out, @err = shell.execute command + @status = shell.exit_status + puts "STATUS: [#{@status}]" if @verbose + puts "OUTPUT: [#{@out}]" if @verbose + puts "ERROR: [#{@err}]" if @verbose + puts "PWD: [#{Dir.pwd}]" if @verbose + shell.close + end + + private + + def assert_status(expected_status=0) + assert_equal expected_status, @status + end + +end diff --git a/test/rubygems/gemutilities.rb b/test/rubygems/gemutilities.rb new file mode 100644 index 0000000000..96711be485 --- /dev/null +++ b/test/rubygems/gemutilities.rb @@ -0,0 +1,295 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +at_exit { $SAFE = 1 } + +require 'fileutils' +require 'test/unit/testcase' +require 'tmpdir' +require 'uri' +require 'rubygems/gem_open_uri' +require 'rubygems/source_info_cache' + +require File.join(File.expand_path(File.dirname(__FILE__)), 'mockgemui') + +module Gem + def self.source_index=(si) + @@source_index = si + end +end + +class FakeFetcher + + attr_reader :data + attr_accessor :uri + attr_accessor :paths + + def initialize + @data = {} + @paths = [] + @uri = nil + end + + def fetch_path(path) + path = path.to_s + @paths << path + raise ArgumentError, 'need full URI' unless path =~ %r'^http://' + data = @data[path] + raise OpenURI::HTTPError.new("no data for #{path}", nil) if data.nil? + data.respond_to?(:call) ? data.call : data + end + + def fetch_size(path) + path = path.to_s + @paths << path + raise ArgumentError, 'need full URI' unless path =~ %r'^http://' + data = @data[path] + raise OpenURI::HTTPError.new("no data for #{path}", nil) if data.nil? + data.respond_to?(:call) ? data.call : data.length + end + +end + +class RubyGemTestCase < Test::Unit::TestCase + + include Gem::DefaultUserInteraction + + undef_method :default_test + + def setup + super + + @ui = MockGemUi.new + tmpdir = nil + Dir.chdir Dir.tmpdir do tmpdir = Dir.pwd end # HACK OSX /private/tmp + @tempdir = File.join tmpdir, "test_rubygems_#{$$}" + @tempdir.untaint + @gemhome = File.join @tempdir, "gemhome" + @gemcache = File.join(@gemhome, "source_cache") + @usrcache = File.join(@gemhome, ".gem", "user_cache") + + FileUtils.mkdir_p @gemhome + + ENV['GEMCACHE'] = @usrcache + Gem.use_paths(@gemhome) + Gem.loaded_specs.clear + + Gem.configuration.verbose = true + Gem.configuration.update_sources = true + + @gem_repo = "http://gems.example.com" + Gem.sources.replace [@gem_repo] + + @orig_arch = Config::CONFIG['arch'] + + if win_platform? + util_set_arch 'i386-mswin32' + else + util_set_arch 'i686-darwin8.10.1' + end + + @marshal_version = "#{Marshal::MAJOR_VERSION}.#{Marshal::MINOR_VERSION}" + end + + def teardown + Config::CONFIG['arch'] = @orig_arch + + if defined? Gem::RemoteFetcher then + Gem::RemoteFetcher.instance_variable_set :@fetcher, nil + end + + FileUtils.rm_rf @tempdir + + ENV.delete 'GEMCACHE' + ENV.delete 'GEM_HOME' + ENV.delete 'GEM_PATH' + + Gem.clear_paths + Gem::SourceInfoCache.instance_variable_set :@cache, nil + end + + def install_gem gem + require 'rubygems/installer' + + use_ui MockGemUi.new do + Dir.chdir @tempdir do + Gem::Builder.new(gem).build + end + end + + gem = File.join(@tempdir, "#{gem.full_name}.gem").untaint + Gem::Installer.new(gem).install + end + + def prep_cache_files(lc) + [ [lc.system_cache_file, 'sys'], + [lc.user_cache_file, 'usr'], + ].each do |fn, data| + FileUtils.mkdir_p File.dirname(fn).untaint + open(fn.dup.untaint, "wb") { |f| f.write(Marshal.dump({'key' => data})) } + end + end + + def read_cache(fn) + open(fn.dup.untaint) { |f| Marshal.load f.read } + end + + def write_file(path) + path = File.join(@gemhome, path) + dir = File.dirname path + FileUtils.mkdir_p dir + File.open(path, "w") { |io| + yield(io) + } + path + end + + def quick_gem(gemname, version='0.0.2') + require 'rubygems/specification' + + spec = Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = gemname + s.version = version + s.author = 'A User' + s.email = 'example@example.com' + s.homepage = 'http://example.com' + s.has_rdoc = true + s.summary = "this is a summary" + s.description = "This is a test description" + yield(s) if block_given? + end + + path = File.join "specifications", "#{spec.full_name}.gemspec" + written_path = write_file path do |io| + io.write(spec.to_ruby) + end + + spec.loaded_from = written_path + + return spec + end + + def util_build_gem(spec) + dir = File.join(@gemhome, 'gems', spec.full_name) + FileUtils.mkdir_p dir + + Dir.chdir dir do + spec.files.each do |file| + next if File.exist? file + FileUtils.mkdir_p File.dirname(file) + File.open file, 'w' do |fp| fp.puts "# #{file}" end + end + + use_ui MockGemUi.new do + Gem::Builder.new(spec).build + end + + FileUtils.mv "#{spec.full_name}.gem", File.join(@gemhome, 'cache') + end + end + + def util_make_gems + spec = proc do |s| + s.files = %w[lib/code.rb] + s.require_paths = %w[lib] + end + + @a0_0_1 = quick_gem('a', '0.0.1', &spec) + @a0_0_2 = quick_gem('a', '0.0.2', &spec) + @b0_0_2 = quick_gem('b', '0.0.2', &spec) + @c1_2 = quick_gem('c', '1.2', &spec) + + write_file File.join(*%w[gems a-0.0.1 lib code.rb]) do end + write_file File.join(*%w[gems a-0.0.2 lib code.rb]) do end + write_file File.join(*%w[gems b-0.0.2 lib code.rb]) do end + write_file File.join(*%w[gems c-1.2 lib code.rb]) do end + + [@a0_0_1, @a0_0_2, @b0_0_2, @c1_2].each { |spec| util_build_gem spec } + + Gem.source_index = nil + end + + ## + # Set the platform to +cpu+ and +os+ + + def util_set_arch(arch) + Config::CONFIG['arch'] = arch + platform = Gem::Platform.new arch + + Gem.instance_variable_set :@platforms, nil + Gem::Platform.instance_variable_set :@local, nil + + platform + end + + def util_setup_fake_fetcher + require 'zlib' + require 'socket' + require 'rubygems/remote_fetcher' + + @uri = URI.parse @gem_repo + @fetcher = FakeFetcher.new + @fetcher.uri = @uri + + @gem1 = quick_gem 'gem_one' do |gem| + gem.files = %w[Rakefile lib/gem_one.rb] + end + + @gem2 = quick_gem 'gem_two' do |gem| + gem.files = %w[Rakefile lib/gem_two.rb] + end + + @gem3 = quick_gem 'gem_three' do |gem| # missing gem + gem.files = %w[Rakefile lib/gem_three.rb] + end + + # this gem has a higher version and longer name than the gem we want + @gem4 = quick_gem 'gem_one_evil', '666' do |gem| + gem.files = %w[Rakefile lib/gem_one.rb] + end + + @all_gems = [@gem1, @gem2, @gem3, @gem4].sort + @all_gem_names = @all_gems.map { |gem| gem.full_name } + + gem_names = [@gem1.full_name, @gem2.full_name, @gem4.full_name] + @gem_names = gem_names.sort.join("\n") + + @source_index = Gem::SourceIndex.new @gem1.full_name => @gem1, + @gem2.full_name => @gem2, + @gem4.full_name => @gem4 + + Gem::RemoteFetcher.instance_variable_set :@fetcher, @fetcher + end + + def util_setup_source_info_cache(*specs) + require 'rubygems/source_info_cache_entry' + + specs = Hash[*specs.map { |spec| [spec.full_name, spec] }.flatten] + si = Gem::SourceIndex.new specs + + sice = Gem::SourceInfoCacheEntry.new si, 0 + sic = Gem::SourceInfoCache.new + sic.set_cache_data( { @gem_repo => sice } ) + Gem::SourceInfoCache.instance_variable_set :@cache, sic + si + end + + def util_zip(data) + Zlib::Deflate.deflate data + end + + def self.win_platform? + Gem.win_platform? + end + + def win_platform? + Gem.win_platform? + end + +end + diff --git a/test/rubygems/insure_session.rb b/test/rubygems/insure_session.rb new file mode 100644 index 0000000000..e56f9abcb8 --- /dev/null +++ b/test/rubygems/insure_session.rb @@ -0,0 +1,51 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + + +require 'rubygems' + +def install_session + path_to_gem = File.join("redist", "session.gem") + begin + Gem::Installer.new(path_to_gem).install + rescue Errno::EACCES => ex + puts + puts "*****************************************************************" + puts "Unable to install Gem 'Session'." + puts "Reason: #{ex.message}" + puts "Try running:" + puts + puts " gem -Li #{path_to_gem}" + puts + puts "with the appropriate admin privileges." + puts "*****************************************************************" + puts + exit + end + gem 'session' +end + +begin + require 'session' +rescue LoadError => e + puts + puts "Required Gem 'Session' missing." + puts "We can attempt to install from the RubyGems Distribution," + puts "but installation may require admin privileges on your system." + puts + print "Install now from RubyGems distribution? [Yn]" + answer = gets + if(answer =~ /^y/i || answer =~ /^[^a-zA-Z0-9]$/) then + install_session + puts + puts "Retry running the functional tests." + exit(0) + else + puts "Test cancelled...quitting" + exit(1) + end +end diff --git a/test/rubygems/mockgemui.rb b/test/rubygems/mockgemui.rb new file mode 100644 index 0000000000..d9bc2a8134 --- /dev/null +++ b/test/rubygems/mockgemui.rb @@ -0,0 +1,51 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + + +require 'stringio' +require 'rubygems/user_interaction' + +class MockGemUi < Gem::StreamUI + class TermError < RuntimeError; end + + def initialize(input="") + super(StringIO.new(input), StringIO.new, StringIO.new) + @terminated = false + @banged = false + end + + def input + @ins.string + end + + def output + @outs.string + end + + def error + @errs.string + end + + def banged? + @banged + end + + def terminated? + @terminated + end + + def terminate_interaction!(status=1) + @terminated = true + @banged = true + fail TermError + end + + def terminate_interaction(status=0) + @terminated = true + fail TermError + end +end diff --git a/test/rubygems/simple_gem.rb b/test/rubygems/simple_gem.rb new file mode 100644 index 0000000000..a6f14bc3c7 --- /dev/null +++ b/test/rubygems/simple_gem.rb @@ -0,0 +1,72 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + + SIMPLE_GEM = <<-GEMDATA + MD5SUM = "e3701f9db765a2358aef94c40ded71c8" + if $0 == __FILE__ + require 'optparse' + + options = {} + ARGV.options do |opts| + opts.on_tail("--help", "show this message") {puts opts; exit} + opts.on('--dir=DIRNAME', "Installation directory for the Gem") {|options[:directory]|} + opts.on('--force', "Force Gem to intall, bypassing dependency checks") {|options[:force]|} + opts.on('--gen-rdoc', "Generate RDoc documentation for the Gem") {|options[:gen_rdoc]|} + opts.parse! + end + + require 'rubygems' + @directory = options[:directory] || Gem.dir + @force = options[:force] + + gem = Gem::Installer.new(__FILE__).install(@force, @directory) + if options[:gen_rdoc] + Gem::DocManager.new(gem).generate_rdoc + end +end + +__END__ +--- !ruby/object:Gem::Specification +rubygems_version: "1.0" +name: testing +version: !ruby/object:Gem::Version + version: 1.2.3 +date: 2004-03-18 22:01:52.859121 -05:00 +platform: +summary: This exercise the gem testing stuff. +require_paths: + - lib +files: + - lib/foo.rb + - lib/test + - lib/test.rb + - lib/test/wow.rb +autorequire: test +test_suite_file: foo +requirements: + - a computer processor +--- +- + size: 109 + mode: 420 + path: lib/foo.rb +- + size: 0 + mode: 420 + path: lib/test.rb +- + size: 15 + mode: 420 + path: lib/test/wow.rb +--- +eJwVjDEKgDAUQ/eeIpsKguhY3ARPoHMp9quF0mL7e39/h5DwQpLpqz4TOqbC +U42eO6WuYEvBntIhECuaaX1KqXXLmy2kAEc32szExK+PjyBAlpTZyK0N/Twu +g1CKTjX9BGAj1w== +--- +eJwDAAAAAAE= +--- +eJwrKC0pVlAvzy9XyE3MU+cCACwiBP4= + GEMDATA diff --git a/test/rubygems/test_config.rb b/test/rubygems/test_config.rb new file mode 100644 index 0000000000..89ac0e4462 --- /dev/null +++ b/test/rubygems/test_config.rb @@ -0,0 +1,26 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rbconfig' +require 'rubygems' + +class TestConfig < RubyGemTestCase + + def test_gem_original_datadir + datadir = Config::CONFIG['datadir'] + assert_equal "#{datadir}/xyz", Config.gem_original_datadir('xyz') + end + + def test_datadir + datadir = Config::CONFIG['datadir'] + assert_equal "#{datadir}/xyz", Config.datadir('xyz') + end + +end + diff --git a/test/rubygems/test_gem.rb b/test/rubygems/test_gem.rb new file mode 100644 index 0000000000..723b2559bc --- /dev/null +++ b/test/rubygems/test_gem.rb @@ -0,0 +1,367 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems' +require 'rubygems/gem_openssl' +require 'pathname' + +class TestGem < RubyGemTestCase + + def setup + super + + @additional = %w[a b].map { |d| File.join @tempdir, d } + @default_dir_re = %r|/ruby/gems/[0-9.]+| + end + + def test_self_all_load_paths + util_make_gems + + expected = [ + File.join(@tempdir, *%w[gemhome gems a-0.0.1 lib]), + File.join(@tempdir, *%w[gemhome gems a-0.0.2 lib]), + File.join(@tempdir, *%w[gemhome gems b-0.0.2 lib]), + File.join(@tempdir, *%w[gemhome gems c-1.2 lib]), + ] + + assert_equal expected, Gem.all_load_paths.sort + end + + def test_self_bindir + assert_equal File.join(@gemhome, 'bin'), Gem.bindir + assert_equal File.join(@gemhome, 'bin'), Gem.bindir(Gem.dir) + assert_equal File.join(@gemhome, 'bin'), Gem.bindir(Pathname.new(Gem.dir)) + end + + def test_self_bindir_default_dir + default = Gem.default_dir + assert_equal Config::CONFIG['bindir'], Gem.bindir(default) + assert_equal Config::CONFIG['bindir'], Gem.bindir(Pathname.new(default)) + end + + def test_self_clear_paths + Gem.dir + Gem.path + searcher = Gem.searcher + source_index = Gem.source_index + + Gem.clear_paths + + assert_equal nil, Gem.instance_variable_get(:@gem_home) + assert_equal nil, Gem.instance_variable_get(:@gem_path) + assert_not_equal searcher, Gem.searcher + assert_not_equal source_index, Gem.source_index + end + + def test_self_configuration + expected = Gem::ConfigFile.new [] + Gem.configuration = nil + + assert_equal expected, Gem.configuration + end + + def test_self_datadir + foo = nil + + Dir.chdir @tempdir do + FileUtils.mkdir_p 'data' + File.open File.join('data', 'foo.txt'), 'w' do |fp| + fp.puts 'blah' + end + + foo = quick_gem 'foo' do |s| s.files = %w[data/foo.txt] end + install_gem foo + end + + gem 'foo' + + expected = File.join @gemhome, 'gems', foo.full_name, 'data', 'foo' + + assert_equal expected, Gem.datadir('foo') + end + + def test_self_datadir_nonexistent_package + assert_nil Gem.datadir('xyzzy') + end + + def test_self_default_dir + assert_match @default_dir_re, Gem.default_dir + end + + def test_self_default_sources + assert_equal %w[http://gems.rubyforge.org], Gem.default_sources + end + + def test_self_dir + assert_equal @gemhome, Gem.dir + + Gem::DIRECTORIES.each do |filename| + assert File.directory?(File.join(Gem.dir, filename)), + "expected #{filename} to exist" + end + end + + def test_self_ensure_gem_directories + FileUtils.rm_r @gemhome + Gem.use_paths @gemhome + + Gem.ensure_gem_subdirectories @gemhome + + assert File.directory?(File.join(@gemhome, "cache")) + end + + def test_self_ensure_gem_directories_missing_parents + gemdir = File.join @tempdir, 'a/b/c/gemdir' + FileUtils.rm_rf File.join(@tempdir, 'a') rescue nil + assert !File.exist?(File.join(@tempdir, 'a')), + "manually remove #{File.join @tempdir, 'a'}, tests are broken" + Gem.use_paths gemdir + + Gem.ensure_gem_subdirectories gemdir + + assert File.directory?("#{gemdir}/cache") + end + + unless win_platform? then # only for FS that support write protection + def test_self_ensure_gem_directories_write_protected + gemdir = File.join @tempdir, "egd" + FileUtils.rm_r gemdir rescue nil + assert !File.exist?(gemdir), "manually remove #{gemdir}, tests are broken" + FileUtils.mkdir_p gemdir + FileUtils.chmod 0400, gemdir + Gem.use_paths gemdir + + Gem.ensure_gem_subdirectories gemdir + + assert !File.exist?("#{gemdir}/cache") + ensure + FileUtils.chmod 0600, gemdir + end + + def test_self_ensure_gem_directories_write_protected_parents + parent = File.join(@tempdir, "egd") + gemdir = "#{parent}/a/b/c" + + FileUtils.rm_r parent rescue nil + assert !File.exist?(parent), "manually remove #{parent}, tests are broken" + FileUtils.mkdir_p parent + FileUtils.chmod 0400, parent + Gem.use_paths(gemdir) + + Gem.ensure_gem_subdirectories gemdir + + assert !File.exist?("#{gemdir}/cache") + ensure + FileUtils.chmod 0600, parent + end + end + + def test_ensure_ssl_available + orig_Gem_ssl_available = Gem.ssl_available? + + Gem.ssl_available = true + assert_nothing_raised do Gem.ensure_ssl_available end + + Gem.ssl_available = false + e = assert_raise Gem::Exception do Gem.ensure_ssl_available end + assert_equal 'SSL is not installed on this system', e.message + ensure + Gem.ssl_available = orig_Gem_ssl_available + end + + def test_self_latest_load_paths + util_make_gems + + expected = [ + File.join(@tempdir, *%w[gemhome gems a-0.0.2 lib]), + File.join(@tempdir, *%w[gemhome gems b-0.0.2 lib]), + File.join(@tempdir, *%w[gemhome gems c-1.2 lib]), + ] + + assert_equal expected, Gem.latest_load_paths.sort + end + + def test_self_loaded_specs + foo = quick_gem 'foo' + install_gem foo + Gem.source_index = nil + + Gem.activate 'foo', false + + assert_equal true, Gem.loaded_specs.keys.include?('foo') + end + + def test_self_path + assert_equal [Gem.dir], Gem.path + end + + def test_self_path_ENV_PATH + Gem.clear_paths + util_ensure_gem_dirs + + ENV['GEM_PATH'] = @additional.join(File::PATH_SEPARATOR) + + assert_equal @additional, Gem.path[0,2] + assert_equal 3, Gem.path.size + assert_match Gem.dir, Gem.path.last + end + + def test_self_path_duplicate + Gem.clear_paths + util_ensure_gem_dirs + dirs = @additional + [@gemhome] + [File.join(@tempdir, 'a')] + + ENV['GEM_HOME'] = @gemhome + ENV['GEM_PATH'] = dirs.join File::PATH_SEPARATOR + + assert_equal @gemhome, Gem.dir + assert_equal @additional + [Gem.dir], Gem.path + end + + def test_self_path_overlap + Gem.clear_paths + + util_ensure_gem_dirs + ENV['GEM_HOME'] = @gemhome + ENV['GEM_PATH'] = @additional.join(File::PATH_SEPARATOR) + + assert_equal @gemhome, Gem.dir + assert_equal @additional + [Gem.dir], Gem.path + end + + def test_self_platforms + assert_equal [Gem::Platform::RUBY, Gem::Platform.local], Gem.platforms + end + + def test_self_prefix + file_name = File.expand_path __FILE__ + assert_equal File.dirname(File.dirname(file_name)), Gem.prefix + end + + def test_self_required_location + util_make_gems + + assert_equal File.join(@tempdir, *%w[gemhome gems c-1.2 lib code.rb]), + Gem.required_location("c", "code.rb") + assert_equal File.join(@tempdir, *%w[gemhome gems a-0.0.1 lib code.rb]), + Gem.required_location("a", "code.rb", "<0.0.2") + assert_equal File.join(@tempdir, *%w[gemhome gems a-0.0.2 lib code.rb]), + Gem.required_location("a", "code.rb", "=0.0.2") + end + + def test_self_searcher + assert_kind_of Gem::GemPathSearcher, Gem.searcher + end + + def test_self_source_index + assert_kind_of Gem::SourceIndex, Gem.source_index + end + + def test_self_sources + assert_equal %w[http://gems.example.com], Gem.sources + end + + def test_ssl_available_eh + orig_Gem_ssl_available = Gem.ssl_available? + + Gem.ssl_available = true + assert_equal true, Gem.ssl_available? + + Gem.ssl_available = false + assert_equal false, Gem.ssl_available? + ensure + Gem.ssl_available = orig_Gem_ssl_available + end + + def test_self_use_paths + util_ensure_gem_dirs + + Gem.use_paths @gemhome, @additional + + assert_equal @gemhome, Gem.dir + assert_equal @additional + [Gem.dir], Gem.path + end + + def test_self_user_home + if ENV['HOME'] then + assert_equal ENV['HOME'], Gem.user_home + else + assert true, 'count this test' + end + end + + def test_require_gem_autorequire + name = "AutorequireArray" + files = %w(a.rb b.rb) + gem = quick_gem(name) do |s| + s.files = files.map { |f| File.join("lib", f) } + s.autorequire = files + end + + fullname = gem.full_name + + write_file "gems/#{fullname}/lib/a.rb" do |io| + io.puts "$LOADED_A = true" + end + + write_file "gems/#{fullname}/lib/b.rb" do |io| + io.puts "$LOADED_B = true" + end + + Gem.source_index = nil + + old_loaded = $".dup + old_verbose = $VERBOSE + $VERBOSE = nil + require_gem name + $VERBOSE = old_verbose + new_loaded = $".dup + + if RUBY_VERSION > "1.9" then + files = files.map do |file| + File.join @gemhome, 'gems', gem.full_name, 'lib', file + end + end + + assert_equal files, (new_loaded - old_loaded) + assert defined?($LOADED_A) + assert defined?($LOADED_B) + end + + def test_require_gem_autorequire_string + name = "AutorequireString" + file = "c.rb" + gem = quick_gem(name) do |s| + s.files = File.join("lib", file) + s.autorequire = file + end + + fullname = gem.full_name + + write_file("gems/#{fullname}/lib/c.rb") do |io| + io.puts "$LOADED_C = true" + end + + old_loaded = $".dup + old_verbose = $VERBOSE + $VERBOSE = nil + require_gem name + $VERBOSE = old_verbose + new_loaded = $".dup + + if RUBY_VERSION > "1.9" then + file = File.join @gemhome, 'gems', gem.full_name, 'lib', file + end + + assert_equal(Array(file), (new_loaded - old_loaded)) + assert(defined? $LOADED_C) + end + + def util_ensure_gem_dirs + Gem.ensure_gem_subdirectories @gemhome + @additional.each do |dir| + Gem.ensure_gem_subdirectories @gemhome + end + end + +end + diff --git a/test/rubygems/test_gem_builder.rb b/test/rubygems/test_gem_builder.rb new file mode 100644 index 0000000000..31a0d71880 --- /dev/null +++ b/test/rubygems/test_gem_builder.rb @@ -0,0 +1,34 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/builder' + +class TestGemBuilder < RubyGemTestCase + + def test_build + builder = Gem::Builder.new quick_gem('a') + + use_ui @ui do + Dir.chdir @tempdir do + builder.build + end + end + + assert_match %r|Successfully built RubyGem\n Name: a|, @ui.output + end + + def test_build_validates + builder = Gem::Builder.new Gem::Specification.new + + assert_raises Gem::InvalidSpecificationException do + builder.build + end + end + +end + diff --git a/test/rubygems/test_gem_command.rb b/test/rubygems/test_gem_command.rb new file mode 100644 index 0000000000..9ed57b3692 --- /dev/null +++ b/test/rubygems/test_gem_command.rb @@ -0,0 +1,196 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/command' + +class Gem::Command + public :parser +end + +class TestGemCommand < RubyGemTestCase + + def setup + super + + @xopt = nil + + Gem::Command.common_options.clear + Gem::Command.common_options << [ + ['-x', '--exe', 'Execute'], lambda do |*a| + @xopt = true + end + ] + + @cmd_name = 'doit' + @cmd = Gem::Command.new @cmd_name, 'summary' + end + + def test_self_add_specific_extra_args + added_args = %w[--all] + @cmd.add_option '--all' do |v,o| end + + Gem::Command.add_specific_extra_args @cmd_name, added_args + + assert_equal added_args, Gem::Command.specific_extra_args(@cmd_name) + + h = @cmd.add_extra_args [] + + assert_equal added_args, h + end + + def test_self_add_specific_extra_args_unknown + added_args = %w[--definitely_not_there] + + Gem::Command.add_specific_extra_args @cmd_name, added_args + + assert_equal added_args, Gem::Command.specific_extra_args(@cmd_name) + + h = @cmd.add_extra_args [] + + assert_equal [], h + end + + def test_add_option_overlapping_common_and_local_options + @cmd.add_option('-x', '--zip', 'BAD!') do end + @cmd.add_option('-z', '--exe', 'BAD!') do end + @cmd.add_option('-x', '--exe', 'BAD!') do end + + assert_match %r|-x, --zip|, @cmd.parser.to_s + assert_match %r|-z, --exe|, @cmd.parser.to_s + assert_no_match %r|-x, --exe|, @cmd.parser.to_s + end + + def test_basic_accessors + assert_equal "doit", @cmd.command + assert_equal "gem doit", @cmd.program_name + assert_equal "summary", @cmd.summary + end + + def test_common_option_in_class + assert Array === Gem::Command.common_options + end + + def test_defaults + @cmd.add_option('-h', '--help [COMMAND]', 'Get help on COMMAND') do |value, options| + options[:help] = value + end + + @cmd.defaults = { :help => true } + + @cmd.when_invoked do |options| + assert options[:help], "Help options should default true" + end + + use_ui @ui do + @cmd.invoke + end + + assert_match %r|Usage: gem doit|, @ui.output + end + + def test_invoke + done = false + @cmd.when_invoked { done = true } + + use_ui @ui do + @cmd.invoke + end + + assert done + end + + def test_invode_with_bad_options + use_ui @ui do + @cmd.when_invoked do true end + + ex = assert_raise(OptionParser::InvalidOption) do + @cmd.invoke('-zzz') + end + + assert_match(/invalid option:/, ex.message) + end + end + + def test_invoke_with_common_options + @cmd.when_invoked do true end + + use_ui @ui do + @cmd.invoke "-x" + end + + assert @xopt, "Should have done xopt" + end + + # Returning false from the command handler invokes the usage output. + def test_invoke_with_help + done = false + + use_ui @ui do + @cmd.add_option('-h', '--help [COMMAND]', 'Get help on COMMAND') do |value, options| + options[:help] = true + done = true + end + + @cmd.invoke('--help') + + assert done + end + + assert_match(/Usage/, @ui.output) + assert_match(/gem doit/, @ui.output) + assert_match(/\[options\]/, @ui.output) + assert_match(/-h/, @ui.output) + assert_match(/--help \[COMMAND\]/, @ui.output) + assert_match(/Get help on COMMAND/, @ui.output) + assert_match(/-x/, @ui.output) + assert_match(/--exe/, @ui.output) + assert_match(/Execute/, @ui.output) + assert_match(/Common Options:/, @ui.output) + end + + def test_invoke_with_options + @cmd.add_option('-h', '--help [COMMAND]', 'Get help on COMMAND') do |value, options| + options[:help] = true + end + + @cmd.when_invoked do |opts| + assert opts[:help] + end + + use_ui @ui do + @cmd.invoke '-h' + end + + assert_match %r|Usage: gem doit|, @ui.output + end + + def test_option_recognition + @cmd.add_option('-h', '--help [COMMAND]', 'Get help on COMMAND') do |value, options| + options[:help] = true + end + @cmd.add_option('-f', '--file FILE', 'File option') do |value, options| + options[:help] = true + end + assert @cmd.handles?(['-x']) + assert @cmd.handles?(['-h']) + assert @cmd.handles?(['-h', 'command']) + assert @cmd.handles?(['--help', 'command']) + assert @cmd.handles?(['-f', 'filename']) + assert @cmd.handles?(['--file=filename']) + assert ! @cmd.handles?(['-z']) + assert ! @cmd.handles?(['-f']) + assert ! @cmd.handles?(['--toothpaste']) + + args = ['-h', 'command'] + @cmd.handles?(args) + assert_equal ['-h', 'command'], args + end + +end + diff --git a/test/rubygems/test_gem_command_manager.rb b/test/rubygems/test_gem_command_manager.rb new file mode 100644 index 0000000000..4198bb9a2a --- /dev/null +++ b/test/rubygems/test_gem_command_manager.rb @@ -0,0 +1,211 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/command_manager' + +class InterruptCommand < Gem::Command + + def initialize + super('interrupt', 'Raises an Interrupt Exception', {}) + end + + def execute + raise Interrupt, "Interrupt exception" + end + +end + +class TestGemCommandManager < RubyGemTestCase + + def setup + super + + @command_manager = Gem::CommandManager.new + end + + def test_run_interrupt + use_ui @ui do + @command_manager.register_command :interrupt + assert_raises MockGemUi::TermError do + @command_manager.run 'interrupt' + end + assert_equal '', ui.output + assert_equal "ERROR: Interrupted\n", ui.error + end + end + + def test_process_args_bad_arg + use_ui @ui do + assert_raises(MockGemUi::TermError) { + @command_manager.process_args("--bad-arg") + } + end + + assert_match(/invalid option: --bad-arg/i, @ui.error) + end + + def test_process_args_install + #capture all install options + use_ui @ui do + check_options = nil + @command_manager['install'].when_invoked do |options| + check_options = options + true + end + + #check defaults + @command_manager.process_args("install") + assert_equal false, check_options[:test] + assert_equal true, check_options[:generate_rdoc] + assert_equal false, check_options[:force] + assert_equal :both, check_options[:domain] + assert_equal true, check_options[:wrappers] + assert_equal Gem::Requirement.default, check_options[:version] + assert_equal Gem.dir, check_options[:install_dir] + + #check settings + check_options = nil + @command_manager.process_args( + "install --force --test --local --rdoc --install-dir . --version 3.0 --no-wrapper") + assert_equal true, check_options[:test] + assert_equal true, check_options[:generate_rdoc] + assert_equal true, check_options[:force] + assert_equal :local, check_options[:domain] + assert_equal false, check_options[:wrappers] + assert_equal Gem::Requirement.new('3.0'), check_options[:version] + assert_equal Dir.pwd, check_options[:install_dir] + + #check remote domain + check_options = nil + @command_manager.process_args("install --remote") + assert_equal :remote, check_options[:domain] + + #check both domain + check_options = nil + @command_manager.process_args("install --both") + assert_equal :both, check_options[:domain] + + #check both domain + check_options = nil + @command_manager.process_args("install --both") + assert_equal :both, check_options[:domain] + end + end + + def test_process_args_uninstall + #capture all uninstall options + check_options = nil + @command_manager['uninstall'].when_invoked do |options| + check_options = options + true + end + + #check defaults + @command_manager.process_args("uninstall") + assert_equal Gem::Requirement.default, check_options[:version] + + #check settings + check_options = nil + @command_manager.process_args("uninstall foobar --version 3.0") + assert_equal "foobar", check_options[:args].first + assert_equal Gem::Requirement.new('3.0'), check_options[:version] + end + + def test_process_args_check + #capture all check options + check_options = nil + @command_manager['check'].when_invoked do |options| + check_options = options + true + end + + #check defaults + @command_manager.process_args("check") + assert_equal false, check_options[:verify] + assert_equal false, check_options[:alien] + + #check settings + check_options = nil + @command_manager.process_args("check --verify foobar --alien") + assert_equal "foobar", check_options[:verify] + assert_equal true, check_options[:alien] + end + + def test_process_args_build + #capture all build options + check_options = nil + @command_manager['build'].when_invoked do |options| + check_options = options + true + end + + #check defaults + @command_manager.process_args("build") + #NOTE: Currently no defaults + + #check settings + check_options = nil + @command_manager.process_args("build foobar.rb") + assert_equal 'foobar.rb', check_options[:args].first + end + + def test_process_args_query + #capture all query options + check_options = nil + @command_manager['query'].when_invoked do |options| + check_options = options + true + end + + #check defaults + @command_manager.process_args("query") + assert_equal(/.*/, check_options[:name]) + assert_equal :local, check_options[:domain] + assert_equal false, check_options[:details] + + #check settings + check_options = nil + @command_manager.process_args("query --name foobar --local --details") + assert_equal(/foobar/i, check_options[:name]) + assert_equal :local, check_options[:domain] + assert_equal true, check_options[:details] + + #remote domain + check_options = nil + @command_manager.process_args("query --remote") + assert_equal :remote, check_options[:domain] + + #both (local/remote) domains + check_options = nil + @command_manager.process_args("query --both") + assert_equal :both, check_options[:domain] + end + + def test_process_args_update + #capture all update options + check_options = nil + @command_manager['update'].when_invoked do |options| + check_options = options + true + end + + #check defaults + @command_manager.process_args("update") + assert_equal true, check_options[:generate_rdoc] + + #check settings + check_options = nil + @command_manager.process_args("update --force --test --rdoc --install-dir .") + assert_equal true, check_options[:test] + assert_equal true, check_options[:generate_rdoc] + assert_equal true, check_options[:force] + assert_equal Dir.pwd, check_options[:install_dir] + end + +end + diff --git a/test/rubygems/test_gem_commands_build_command.rb b/test/rubygems/test_gem_commands_build_command.rb new file mode 100644 index 0000000000..f1fd1503ba --- /dev/null +++ b/test/rubygems/test_gem_commands_build_command.rb @@ -0,0 +1,75 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/commands/build_command' +require 'rubygems/format' + +class TestGemCommandsBuildCommand < RubyGemTestCase + + def setup + super + + @cmd = Gem::Commands::BuildCommand.new + end + + def test_execute + gem = quick_gem 'some_gem' + + gemspec_file = File.join(@tempdir, "#{gem.full_name}.gemspec") + + File.open gemspec_file, 'w' do |gs| + gs.write gem.to_ruby + end + + util_test_build_gem gem, gemspec_file + end + + def test_execute_yaml + gem = quick_gem 'some_gem' + + gemspec_file = File.join(@tempdir, "#{gem.full_name}.gemspec") + + File.open gemspec_file, 'w' do |gs| + gs.write gem.to_yaml + end + + util_test_build_gem gem, gemspec_file + end + + def test_execute_bad_gem + @cmd.options[:args] = %w[some_gem] + use_ui @ui do + @cmd.execute + end + + assert_equal '', @ui.output + assert_equal "ERROR: Gemspec file not found: some_gem\n", @ui.error + end + + def util_test_build_gem(gem, gemspec_file) + @cmd.options[:args] = [gemspec_file] + + use_ui @ui do + Dir.chdir @tempdir do + @cmd.execute + end + end + + output = @ui.output.split "\n" + assert_equal " Successfully built RubyGem", output.shift + assert_equal " Name: some_gem", output.shift + assert_equal " Version: 0.0.2", output.shift + assert_equal " File: some_gem-0.0.2.gem", output.shift + assert_equal [], output + assert_equal '', @ui.error + + gem_file = File.join @tempdir, "#{gem.full_name}.gem" + assert File.exist?(gem_file) + + spec = Gem::Format.from_file_by_path(gem_file).spec + + assert_equal "some_gem", spec.name + assert_equal "this is a summary", spec.summary + end + +end + diff --git a/test/rubygems/test_gem_commands_cert_command.rb b/test/rubygems/test_gem_commands_cert_command.rb new file mode 100644 index 0000000000..09a401e4f7 --- /dev/null +++ b/test/rubygems/test_gem_commands_cert_command.rb @@ -0,0 +1,122 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') + +require 'rubygems/commands/cert_command' + +class TestGemCommandsCertCommand < RubyGemTestCase + + def setup + super + + @orig_security_trust_dir = Gem::Security::OPT[:trust_dir] + Gem::Security::OPT[:trust_dir] = @tempdir + + @cmd = Gem::Commands::CertCommand.new + + root = File.expand_path(File.dirname(__FILE__)) + + FileUtils.cp File.join(root, 'data', 'gem-private_key.pem'), @tempdir + FileUtils.cp File.join(root, 'data', 'gem-public_cert.pem'), @tempdir + + @cert_file_name = File.join @tempdir, 'gem-public_cert.pem' + @pkey_file_name = File.join @tempdir, 'gem-private_key.pem' + end + + def teardown + Gem::Security::OPT[:trust_dir] = @orig_security_trust_dir + + super + end + + def test_execute_add + use_ui @ui do + @cmd.send :handle_options, %W[--add #{@cert_file_name}] + end + + assert_equal "Added '/CN=rubygems/DC=example/DC=com'\n", @ui.output + assert_equal '', @ui.error + end + + def test_execute_build + FileUtils.rm @cert_file_name + FileUtils.rm @pkey_file_name + + use_ui @ui do + Dir.chdir @tempdir do + @cmd.send :handle_options, %W[--build nobody@example.com] + end + end + + output = @ui.output.split "\n" + + assert_equal 'Public Cert: gem-public_cert.pem', output.shift + assert_equal 'Private Key: gem-private_key.pem', output.shift + assert_equal 'Don\'t forget to move the key file to somewhere private...', + output.shift + assert_equal [], output + + assert_equal '', @ui.error + + assert File.exist?(File.join(@tempdir, 'gem-private_key.pem')) + assert File.exist?(File.join(@tempdir, 'gem-public_cert.pem')) + end + + def test_execute_certificate + use_ui @ui do + @cmd.send :handle_options, %W[--certificate #{@cert_file_name}] + end + + assert_equal '', @ui.output + assert_equal '', @ui.error + + assert_equal File.read(@cert_file_name), + Gem::Security::OPT[:issuer_cert].to_s + end + + def test_execute_list + use_ui @ui do + @cmd.send :handle_options, %W[--list] + end + + assert_equal "/CN=rubygems/DC=example/DC=com\n", @ui.output + assert_equal '', @ui.error + end + + def test_execute_private_key + use_ui @ui do + @cmd.send :handle_options, %W[--private-key #{@pkey_file_name}] + end + + assert_equal '', @ui.output + assert_equal '', @ui.error + + assert_equal File.read(@pkey_file_name), + Gem::Security::OPT[:issuer_key].to_s + end + + def test_execute_remove + use_ui @ui do + @cmd.send :handle_options, %W[--remove rubygems] + end + + assert_equal "Removed '/CN=rubygems/DC=example/DC=com'\n", @ui.output + assert_equal '', @ui.error + + assert !File.exist?(@cert_file_name) + end + + def test_execute_sign + use_ui @ui do + @cmd.send :handle_options, %W[ + -K #{@pkey_file_name} -C #{@cert_file_name} --sign #{@cert_file_name} + ] + end + + assert_equal '', @ui.output + assert_equal '', @ui.error + + # HACK this test sucks + end + +end + diff --git a/test/rubygems/test_gem_commands_check_command.rb b/test/rubygems/test_gem_commands_check_command.rb new file mode 100644 index 0000000000..eea7cc5cfa --- /dev/null +++ b/test/rubygems/test_gem_commands_check_command.rb @@ -0,0 +1,25 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/commands/check_command' + +class TestGemCommandsCheckCommand < RubyGemTestCase + + def setup + super + + @cmd = Gem::Commands::CheckCommand.new + end + + def test_initialize + assert_equal "check", @cmd.command + assert_equal "gem check", @cmd.program_name + assert_match(/Check/, @cmd.summary) + end + +end diff --git a/test/rubygems/test_gem_commands_contents_command.rb b/test/rubygems/test_gem_commands_contents_command.rb new file mode 100644 index 0000000000..cdb89673da --- /dev/null +++ b/test/rubygems/test_gem_commands_contents_command.rb @@ -0,0 +1,92 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/commands/contents_command' + +class TestGemCommandsContentsCommand < RubyGemTestCase + + def setup + super + + @cmd = Gem::Commands::ContentsCommand.new + end + + def test_execute + @cmd.options[:args] = %w[foo] + quick_gem 'foo' do |gem| + gem.files = %w[lib/foo.rb Rakefile] + end + + use_ui @ui do + @cmd.execute + end + + assert_match %r|lib/foo\.rb|, @ui.output + assert_match %r|Rakefile|, @ui.output + assert_equal "", @ui.error + end + + def test_execute_bad_gem + @cmd.options[:args] = %w[foo] + + assert_raise MockGemUi::TermError do + use_ui @ui do + @cmd.execute + end + end + + assert_match %r|Unable to find gem 'foo' in default gem paths|, @ui.output + assert_match %r|Directories searched:|, @ui.output + assert_equal "", @ui.error + end + + def test_execute_exact_match + @cmd.options[:args] = %w[foo] + quick_gem 'foo' do |gem| + gem.files = %w[lib/foo.rb Rakefile] + end + + quick_gem 'foo_bar' do |gem| + gem.files = %w[lib/foo_bar.rb Rakefile] + end + + use_ui @ui do + @cmd.execute + end + + assert_match %r|lib/foo\.rb|, @ui.output + assert_match %r|Rakefile|, @ui.output + assert_equal "", @ui.error + end + + def test_execute_lib_only + @cmd.options[:args] = %w[foo] + @cmd.options[:lib_only] = true + + quick_gem 'foo' do |gem| + gem.files = %w[lib/foo.rb Rakefile] + end + + use_ui @ui do + @cmd.execute + end + + assert_match %r|lib/foo\.rb|, @ui.output + assert_no_match %r|Rakefile|, @ui.output + + assert_equal "", @ui.error + end + + def test_handle_options + assert_equal false, @cmd.options[:lib_only] + assert_equal [], @cmd.options[:specdirs] + assert_equal nil, @cmd.options[:version] + + @cmd.send :handle_options, %w[-l -s foo --version 0.0.2] + + assert_equal true, @cmd.options[:lib_only] + assert_equal %w[foo], @cmd.options[:specdirs] + assert_equal Gem::Requirement.new('0.0.2'), @cmd.options[:version] + end + +end + diff --git a/test/rubygems/test_gem_commands_dependency_command.rb b/test/rubygems/test_gem_commands_dependency_command.rb new file mode 100644 index 0000000000..9bf59537fd --- /dev/null +++ b/test/rubygems/test_gem_commands_dependency_command.rb @@ -0,0 +1,108 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/commands/dependency_command' + +class TestGemCommandsDependencyCommand < RubyGemTestCase + + def setup + super + + @cmd = Gem::Commands::DependencyCommand.new + @cmd.options[:domain] = :local + end + + def test_execute + quick_gem 'foo' do |gem| + gem.add_dependency 'bar', '> 1.0.0' + end + + @cmd.options[:args] = %w[foo] + + use_ui @ui do + @cmd.execute + end + + assert_equal "Gem foo-0.0.2\n bar (> 1.0.0)\n\n", @ui.output + assert_equal '', @ui.error + end + + def test_execute_no_match + @cmd.options[:args] = %w[foo] + + assert_raise MockGemUi::TermError do + use_ui @ui do + @cmd.execute + end + end + + assert_equal "No match found for foo (>= 0)\n", @ui.output + assert_equal '', @ui.error + end + + def test_execute_pipe_format + quick_gem 'foo' do |gem| + gem.add_dependency 'bar', '> 1.0.0' + end + + @cmd.options[:args] = %w[foo] + @cmd.options[:pipe_format] = true + + use_ui @ui do + @cmd.execute + end + + assert_equal "bar --version '> 1.0.0'\n", @ui.output + assert_equal '', @ui.error + end + + def test_execute_reverse + quick_gem 'foo' do |gem| + gem.add_dependency 'bar', '> 1.0.0' + end + + quick_gem 'baz' do |gem| + gem.add_dependency 'foo' + end + + @cmd.options[:args] = %w[foo] + @cmd.options[:reverse_dependencies] = true + + use_ui @ui do + @cmd.execute + end + + expected = <<-EOF +Gem foo-0.0.2 + bar (> 1.0.0) + Used by + baz-0.0.2 (foo (>= 0)) + + EOF + + assert_equal expected, @ui.output + assert_equal '', @ui.error + end + + def test_execute_remote + foo = quick_gem 'foo' do |gem| + gem.add_dependency 'bar', '> 1.0.0' + end + + util_setup_source_info_cache foo + + FileUtils.rm File.join(@gemhome, 'specifications', + "#{foo.full_name}.gemspec") + + @cmd.options[:args] = %w[foo] + @cmd.options[:domain] = :remote + + use_ui @ui do + @cmd.execute + end + + assert_equal "Gem foo-0.0.2\n bar (> 1.0.0)\n\n", @ui.output + assert_equal '', @ui.error + end + +end + diff --git a/test/rubygems/test_gem_commands_environment_command.rb b/test/rubygems/test_gem_commands_environment_command.rb new file mode 100644 index 0000000000..0b97009efa --- /dev/null +++ b/test/rubygems/test_gem_commands_environment_command.rb @@ -0,0 +1,116 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/commands/environment_command' + +class TestGemCommandsEnvironmentCommand < RubyGemTestCase + + def setup + super + + @cmd = Gem::Commands::EnvironmentCommand.new + end + + def test_execute + orig_sources = Gem.sources.dup + Gem.sources.replace %w[http://gems.example.com] + + @cmd.send :handle_options, %w[] + + use_ui @ui do + @cmd.execute + end + + assert_match %r|RUBYGEMS VERSION: (\d\.)+\d \((\d\.)+\d\)|, @ui.output + assert_match %r|RUBY VERSION: \d\.\d\.\d \(.*\) \[.*\]|, @ui.output + assert_match %r|INSTALLATION DIRECTORY: #{@gemhome}|, @ui.output + assert_match %r|RUBYGEMS PREFIX: |, @ui.output + assert_match %r|RUBY EXECUTABLE:.*ruby|, @ui.output + assert_match %r|RUBYGEMS PLATFORMS:|, @ui.output + assert_match %r|- #{Gem::Platform.local}|, @ui.output + assert_match %r|GEM PATHS:|, @ui.output + assert_match %r|- #{@gemhome}|, @ui.output + assert_match %r|GEM CONFIGURATION:|, @ui.output + assert_match %r|:verbose => |, @ui.output + assert_match %r|REMOTE SOURCES:|, @ui.output + assert_equal '', @ui.error + + ensure + Gem.sources.replace orig_sources + end + + def test_execute_gemdir + @cmd.send :handle_options, %w[gemdir] + + use_ui @ui do + @cmd.execute + end + + assert_equal "#{@gemhome}\n", @ui.output + assert_equal '', @ui.error + end + + def test_execute_gempath + @cmd.send :handle_options, %w[gempath] + + use_ui @ui do + @cmd.execute + end + + assert_equal "#{@gemhome}\n", @ui.output + assert_equal '', @ui.error + end + + def test_execute_packageversion + @cmd.send :handle_options, %w[packageversion] + + use_ui @ui do + @cmd.execute + end + + assert_equal "#{Gem::RubyGemsPackageVersion}\n", @ui.output + assert_equal '', @ui.error + end + + def test_execute_remotesources + orig_sources = Gem.sources.dup + Gem.sources.replace %w[http://gems.example.com] + + @cmd.send :handle_options, %w[remotesources] + + use_ui @ui do + @cmd.execute + end + + assert_equal "http://gems.example.com\n", @ui.output + assert_equal '', @ui.error + + ensure + Gem.sources.replace orig_sources + end + + def test_execute_unknown + @cmd.send :handle_options, %w[unknown] + + assert_raise Gem::CommandLineError do + use_ui @ui do + @cmd.execute + end + end + + assert_equal '', @ui.output + assert_equal '', @ui.error + end + + def test_execute_version + @cmd.send :handle_options, %w[version] + + use_ui @ui do + @cmd.execute + end + + assert_equal "#{Gem::RubyGemsVersion}\n", @ui.output + assert_equal '', @ui.error + end + +end + diff --git a/test/rubygems/test_gem_commands_fetch_command.rb b/test/rubygems/test_gem_commands_fetch_command.rb new file mode 100644 index 0000000000..3aa9119378 --- /dev/null +++ b/test/rubygems/test_gem_commands_fetch_command.rb @@ -0,0 +1,34 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/commands/fetch_command' + +class TestGemCommandsFetchCommand < RubyGemTestCase + + def setup + super + + @cmd = Gem::Commands::FetchCommand.new + end + + def test_execute + util_setup_fake_fetcher + + util_build_gem @gem1 + @fetcher.data["#{@gem_repo}/Marshal.#{@marshal_version}"] = + @source_index.dump + @fetcher.data["#{@gem_repo}/gems/#{@gem1.full_name}.gem"] = + File.read(File.join(@gemhome, 'cache', "#{@gem1.full_name}.gem")) + + @cmd.options[:args] = [@gem1.name] + + use_ui @ui do + Dir.chdir @tempdir do + @cmd.execute + end + end + + assert File.exist?(File.join(@tempdir, "#{@gem1.full_name}.gem")) + end + +end + diff --git a/test/rubygems/test_gem_commands_generate_index_command.rb b/test/rubygems/test_gem_commands_generate_index_command.rb new file mode 100644 index 0000000000..548197841b --- /dev/null +++ b/test/rubygems/test_gem_commands_generate_index_command.rb @@ -0,0 +1,32 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/indexer' +require 'rubygems/commands/generate_index_command' + +class TestGemCommandsGenerateIndexCommand < RubyGemTestCase + + def setup + super + + @cmd = Gem::Commands::GenerateIndexCommand.new + @cmd.options[:directory] = @gemhome + end + + def test_execute + use_ui @ui do + @cmd.execute + end + + yaml = File.join @gemhome, 'yaml' + yaml_z = File.join @gemhome, 'yaml.Z' + quick_index = File.join @gemhome, 'quick', 'index' + quick_index_rz = File.join @gemhome, 'quick', 'index.rz' + + assert File.exist?(yaml), yaml + assert File.exist?(yaml_z), yaml_z + assert File.exist?(quick_index), quick_index + assert File.exist?(quick_index_rz), quick_index_rz + end + +end if ''.respond_to? :to_xs + diff --git a/test/rubygems/test_gem_commands_install_command.rb b/test/rubygems/test_gem_commands_install_command.rb new file mode 100644 index 0000000000..78840be8c6 --- /dev/null +++ b/test/rubygems/test_gem_commands_install_command.rb @@ -0,0 +1,160 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/commands/install_command' + +class TestGemCommandsInstallCommand < RubyGemTestCase + + def setup + super + + @cmd = Gem::Commands::InstallCommand.new + @cmd.options[:generate_rdoc] = false + @cmd.options[:generate_ri] = false + end + + def test_execute_include_dependencies + @cmd.options[:include_dependencies] = true + @cmd.options[:args] = [] + + assert_raise Gem::CommandLineError do + use_ui @ui do + @cmd.execute + end + end + + output = @ui.output.split "\n" + assert_equal "INFO: `gem install -y` is now default and will be removed", + output.shift + assert_equal "INFO: use --ignore-dependencies to install only the gems you list", + output.shift + assert output.empty?, output.inspect + end + + def test_execute_local + util_setup_fake_fetcher + @cmd.options[:domain] = :local + + gem1 = quick_gem 'gem_one' + util_build_gem gem1 + FileUtils.mv File.join(@gemhome, 'cache', "#{@gem1.full_name}.gem"), + File.join(@tempdir) + + @cmd.options[:args] = [gem1.name] + + use_ui @ui do + orig_dir = Dir.pwd + begin + Dir.chdir @tempdir + @cmd.execute + ensure + Dir.chdir orig_dir + end + end + + out = @ui.output.split "\n" + assert_equal "Successfully installed #{@gem1.full_name}", out.shift + assert_equal "1 gem installed", out.shift + assert out.empty?, out.inspect + end + + def test_execute_local_missing + util_setup_fake_fetcher + @cmd.options[:domain] = :local + + @cmd.options[:args] = %w[gem_one] + + use_ui @ui do + @cmd.execute + end + + # HACK no repository was checked + assert_equal "ERROR: could not find gem_one locally or in a repository\n", + @ui.error + end + + def test_execute_no_gem + @cmd.options[:args] = %w[] + + assert_raise Gem::CommandLineError do + @cmd.execute + end + end + + def test_execute_nonexistent + util_setup_fake_fetcher + @fetcher.data["#{@gem_repo}/Marshal.#{@marshal_version}"] = + @source_index.dump + + @cmd.options[:args] = %w[nonexistent] + + use_ui @ui do + @cmd.execute + end + + assert_equal "ERROR: could not find nonexistent locally or in a repository\n", + @ui.error + end + + def test_execute_remote + @cmd.options[:generate_rdoc] = true + @cmd.options[:generate_ri] = true + util_setup_fake_fetcher + + util_build_gem @gem1 + @fetcher.data["#{@gem_repo}/Marshal.#{@marshal_version}"] = + @source_index.dump + @fetcher.data["#{@gem_repo}/gems/gem_one-0.0.2.gem"] = + File.read(File.join(@gemhome, 'cache', "#{@gem1.full_name}.gem")) + + @cmd.options[:args] = [@gem1.name] + + use_ui @ui do + @cmd.execute + end + + out = @ui.output.split "\n" + assert_match %r|Bulk updating|, out.shift + assert_equal "Successfully installed #{@gem1.full_name}", out.shift + assert_equal "1 gem installed", out.shift + assert_equal "Installing ri documentation for #{@gem1.full_name}...", + out.shift + assert_equal "Installing RDoc documentation for #{@gem1.full_name}...", + out.shift + assert out.empty?, out.inspect + end + + def test_execute_two + util_setup_fake_fetcher + @cmd.options[:domain] = :local + + gem1 = quick_gem 'gem_one' + util_build_gem gem1 + FileUtils.mv File.join(@gemhome, 'cache', "#{@gem1.full_name}.gem"), + File.join(@tempdir) + + gem2 = quick_gem 'gem_two' + util_build_gem gem2 + FileUtils.mv File.join(@gemhome, 'cache', "#{@gem2.full_name}.gem"), + File.join(@tempdir) + + @cmd.options[:args] = [gem1.name, gem2.name] + + use_ui @ui do + orig_dir = Dir.pwd + begin + Dir.chdir @tempdir + @cmd.execute + ensure + Dir.chdir orig_dir + end + end + + out = @ui.output.split "\n" + assert_equal "Successfully installed #{@gem1.full_name}", out.shift + assert_equal "Successfully installed #{@gem2.full_name}", out.shift + assert_equal "2 gems installed", out.shift + assert out.empty?, out.inspect + end + +end + diff --git a/test/rubygems/test_gem_commands_mirror_command.rb b/test/rubygems/test_gem_commands_mirror_command.rb new file mode 100644 index 0000000000..7ddc8b92f8 --- /dev/null +++ b/test/rubygems/test_gem_commands_mirror_command.rb @@ -0,0 +1,56 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/indexer' +require 'rubygems/commands/mirror_command' + +class TestGemCommandsMirrorCommand < RubyGemTestCase + + def setup + super + + @cmd = Gem::Commands::MirrorCommand.new + end + + def test_execute + util_make_gems + + gems_dir = File.join @tempdir, 'gems' + mirror = File.join @tempdir, 'mirror' + + FileUtils.mkdir_p gems_dir + FileUtils.mkdir_p mirror + + Dir[File.join(@gemhome, 'cache', '*.gem')].each do |gem| + FileUtils.mv gem, gems_dir + end + + use_ui @ui do + Gem::Indexer.new(@tempdir).generate_index + end + + orig_HOME = ENV['HOME'] + ENV['HOME'] = @tempdir + Gem.instance_variable_set :@user_home, nil + + File.open File.join(Gem.user_home, '.gemmirrorrc'), 'w' do |fp| + fp.puts "---" + fp.puts "- from: file://#{@tempdir}" + fp.puts " to: #{mirror}" + end + + use_ui @ui do + @cmd.execute + end + + assert File.exist?(File.join(mirror, 'gems', "#{@a0_0_1.full_name}.gem")) + assert File.exist?(File.join(mirror, 'gems', "#{@a0_0_2.full_name}.gem")) + assert File.exist?(File.join(mirror, 'gems', "#{@b0_0_2.full_name}.gem")) + assert File.exist?(File.join(mirror, 'gems', "#{@c1_2.full_name}.gem")) + assert File.exist?(File.join(mirror, "Marshal.#{@marshal_version}")) + ensure + orig_HOME.nil? ? ENV.delete('HOME') : ENV['HOME'] = orig_HOME + Gem.instance_variable_set :@user_home, nil + end + +end if ''.respond_to? :to_xs + diff --git a/test/rubygems/test_gem_commands_pristine_command.rb b/test/rubygems/test_gem_commands_pristine_command.rb new file mode 100644 index 0000000000..cd1d3500ae --- /dev/null +++ b/test/rubygems/test_gem_commands_pristine_command.rb @@ -0,0 +1,100 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/commands/pristine_command' + +class TestGemCommandsPristineCommand < RubyGemTestCase + + def setup + super + @cmd = Gem::Commands::PristineCommand.new + end + + def test_execute + a = quick_gem 'a' do |s| s.executables = %w[foo] end + FileUtils.mkdir_p File.join(@tempdir, 'bin') + File.open File.join(@tempdir, 'bin', 'foo'), 'w' do |fp| + fp.puts "#!/usr/bin/ruby" + end + + install_gem a + + @cmd.options[:args] = %w[a] + + use_ui @ui do + @cmd.execute + end + + out = @ui.output.split "\n" + + assert_equal "Restoring gem(s) to pristine condition...", out.shift + assert_equal "#{a.full_name} is in pristine condition", out.shift + assert out.empty?, out.inspect + end + + def test_execute_all + a = quick_gem 'a' do |s| s.executables = %w[foo] end + FileUtils.mkdir_p File.join(@tempdir, 'bin') + File.open File.join(@tempdir, 'bin', 'foo'), 'w' do |fp| + fp.puts "#!/usr/bin/ruby" + end + + install_gem a + + gem_bin = File.join @gemhome, 'gems', "#{a.full_name}", 'bin', 'foo' + + FileUtils.rm gem_bin + + @cmd.handle_options %w[--all] + + use_ui @ui do + @cmd.execute + end + + out = @ui.output.split "\n" + + assert_equal "Restoring gem(s) to pristine condition...", out.shift + assert_equal "Restoring 1 file to #{a.full_name}...", out.shift + assert_equal " #{gem_bin}", out.shift + assert out.empty?, out.inspect + end + + def test_execute_missing_cache_gem + a = quick_gem 'a' do |s| s.executables = %w[foo] end + FileUtils.mkdir_p File.join(@tempdir, 'bin') + File.open File.join(@tempdir, 'bin', 'foo'), 'w' do |fp| + fp.puts "#!/usr/bin/ruby" + end + + install_gem a + + FileUtils.rm File.join(@gemhome, 'cache', "#{a.full_name}.gem") + + @cmd.options[:args] = %w[a] + + use_ui @ui do + @cmd.execute + end + + out = @ui.output.split "\n" + + assert_equal "Restoring gem\(s\) to pristine condition...", out.shift + assert out.empty?, out.inspect + + assert_equal "ERROR: Cached gem for #{a.full_name} not found, use `gem install` to restore\n", + @ui.error + end + + def test_execute_no_gem + @cmd.options[:args] = %w[] + + e = assert_raise Gem::CommandLineError do + use_ui @ui do + @cmd.execute + end + end + + assert_match %r|specify a gem name|, e.message + end + +end + diff --git a/test/rubygems/test_gem_commands_query_command.rb b/test/rubygems/test_gem_commands_query_command.rb new file mode 100644 index 0000000000..e2b6a45e92 --- /dev/null +++ b/test/rubygems/test_gem_commands_query_command.rb @@ -0,0 +1,82 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/commands/query_command' + +class TestGemCommandsQueryCommand < RubyGemTestCase + + def setup + super + + @foo_gem = quick_gem 'foo' do |spec| + spec.summary = 'This is a lot of text. ' * 5 + end + @bar_gem = quick_gem 'bar' + + @cmd = Gem::Commands::QueryCommand.new + end + + def test_execute + util_setup_source_info_cache @foo_gem + + @cmd.handle_options %w[-r] + + use_ui @ui do + @cmd.execute + end + + expected = <<-EOF + +*** REMOTE GEMS *** + +foo (0.0.2) + EOF + + assert_equal expected, @ui.output + assert_equal '', @ui.error + end + + def test_execute_details + util_setup_source_info_cache @foo_gem + + @cmd.handle_options %w[-r -d] + + use_ui @ui do + @cmd.execute + end + + expected = <<-EOF + +*** REMOTE GEMS *** + +foo (0.0.2) + This is a lot of text. This is a lot of text. This is a lot of + text. This is a lot of text. This is a lot of text. + EOF + + assert_equal expected, @ui.output + assert_equal '', @ui.error + end + + def test_execute_no_versions + util_setup_source_info_cache @foo_gem, @bar_gem + + @cmd.handle_options %w[-r --no-versions] + + use_ui @ui do + @cmd.execute + end + + expected = <<-EOF + +*** REMOTE GEMS *** + +bar +foo + EOF + + assert_equal expected, @ui.output + assert_equal '', @ui.error + end + +end + diff --git a/test/rubygems/test_gem_commands_sources_command.rb b/test/rubygems/test_gem_commands_sources_command.rb new file mode 100644 index 0000000000..3d1ab801b1 --- /dev/null +++ b/test/rubygems/test_gem_commands_sources_command.rb @@ -0,0 +1,147 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/commands/sources_command' + +class TestGemCommandsSourcesCommand < RubyGemTestCase + + def setup + super + + @cmd = Gem::Commands::SourcesCommand.new + end + + def test_execute + util_setup_source_info_cache + @cmd.handle_options [] + + use_ui @ui do + @cmd.execute + end + + expected = <<-EOF +*** CURRENT SOURCES *** + +#{@gem_repo} + EOF + + assert_equal expected, @ui.output + assert_equal '', @ui.error + end + + def test_execute_add + util_setup_fake_fetcher + + @si = Gem::SourceIndex.new @gem1.full_name => @gem1.name + + @fetcher.data["http://beta-gems.example.com/Marshal.#{@marshal_version}"] = + @si.dump + + @cmd.handle_options %w[--add http://beta-gems.example.com] + + util_setup_source_info_cache + + use_ui @ui do + @cmd.execute + end + + expected = <<-EOF +Bulk updating Gem source index for: http://beta-gems.example.com +http://beta-gems.example.com added to sources + EOF + + assert_equal expected, @ui.output + assert_equal '', @ui.error + + Gem::SourceInfoCache.cache.flush + assert_equal %W[http://beta-gems.example.com #{@gem_repo}], + Gem::SourceInfoCache.cache_data.keys.sort + end + + def test_execute_add_nonexistent_source + util_setup_fake_fetcher + + @si = Gem::SourceIndex.new @gem1.full_name => @gem1.name + + @fetcher.data["http://beta-gems.example.com/Marshal.#{@marshal_version}"] = + proc do + raise Gem::RemoteFetcher::FetchError, 'it died' + end + + + Gem::RemoteFetcher.instance_variable_set :@fetcher, @fetcher + + @cmd.handle_options %w[--add http://beta-gems.example.com] + + util_setup_source_info_cache + + use_ui @ui do + @cmd.execute + end + + expected = <<-EOF +Error fetching http://beta-gems.example.com: +\tit died + EOF + + assert_equal expected, @ui.output + assert_equal '', @ui.error + end + + def test_execute_add_bad_uri + @cmd.handle_options %w[--add beta-gems.example.com] + + util_setup_source_info_cache + + use_ui @ui do + @cmd.execute + end + + expected = <<-EOF +beta-gems.example.com is not a URI + EOF + + assert_equal expected, @ui.output + assert_equal '', @ui.error + end + + def test_execute_remove + @cmd.handle_options %W[--remove #{@gem_repo}] + + util_setup_source_info_cache + + use_ui @ui do + @cmd.execute + end + + expected = "#{@gem_repo} removed from sources\n" + + assert_equal expected, @ui.output + assert_equal '', @ui.error + + Gem::SourceInfoCache.cache.flush + assert_equal [], Gem::SourceInfoCache.cache_data.keys + end + + def test_execute_update + @cmd.handle_options %w[--update] + + util_setup_source_info_cache + util_setup_fake_fetcher + @si = Gem::SourceIndex.new @gem1.full_name => @gem1.name + @fetcher.data["#{@gem_repo}/Marshal.#{@marshal_version}"] = @si.dump + + use_ui @ui do + @cmd.execute + end + + expected = <<-EOF +Bulk updating Gem source index for: #{@gem_repo} +source cache successfully updated + EOF + + assert_equal expected, @ui.output + assert_equal '', @ui.error + end + +end + diff --git a/test/rubygems/test_gem_commands_specification_command.rb b/test/rubygems/test_gem_commands_specification_command.rb new file mode 100644 index 0000000000..3741446536 --- /dev/null +++ b/test/rubygems/test_gem_commands_specification_command.rb @@ -0,0 +1,93 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/commands/specification_command' + +class TestGemCommandsSpecificationCommand < RubyGemTestCase + + def setup + super + + @cmd = Gem::Commands::SpecificationCommand.new + end + + def test_execute + foo = quick_gem 'foo' + + @cmd.options[:args] = %w[foo] + + use_ui @ui do + @cmd.execute + end + + assert_match %r|Gem::Specification|, @ui.output + assert_match %r|name: foo|, @ui.output + assert_equal '', @ui.error + end + + def test_execute_all + foo1 = quick_gem 'foo', '0.0.1' + foo2 = quick_gem 'foo', '0.0.2' + + @cmd.options[:args] = %w[foo] + @cmd.options[:all] = true + + use_ui @ui do + @cmd.execute + end + + assert_match %r|Gem::Specification|, @ui.output + assert_match %r|name: foo|, @ui.output + assert_match %r|version: 0.0.1|, @ui.output + assert_match %r|version: 0.0.2|, @ui.output + assert_equal '', @ui.error + end + + def test_execute_bad_name + @cmd.options[:args] = %w[foo] + + assert_raise MockGemUi::TermError do + use_ui @ui do + @cmd.execute + end + end + + assert_equal '', @ui.output + assert_equal "ERROR: Unknown gem 'foo'\n", @ui.error + end + + def test_execute_exact_match + foo = quick_gem 'foo' + foo_bar = quick_gem 'foo_bar' + + @cmd.options[:args] = %w[foo] + + use_ui @ui do + @cmd.execute + end + + assert_match %r|Gem::Specification|, @ui.output + assert_match %r|name: foo|, @ui.output + assert_equal '', @ui.error + end + + def test_execute_remote + foo = quick_gem 'foo' + + util_setup_source_info_cache foo + + FileUtils.rm File.join(@gemhome, 'specifications', + "#{foo.full_name}.gemspec") + + @cmd.options[:args] = %w[foo] + @cmd.options[:domain] = :remote + + use_ui @ui do + @cmd.execute + end + + assert_equal "#{foo.to_yaml}\n", @ui.output + assert_equal "WARNING: Remote information is not complete\n\n", @ui.error + end + +end + diff --git a/test/rubygems/test_gem_commands_unpack_command.rb b/test/rubygems/test_gem_commands_unpack_command.rb new file mode 100644 index 0000000000..ff3d4e0eae --- /dev/null +++ b/test/rubygems/test_gem_commands_unpack_command.rb @@ -0,0 +1,55 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/commands/unpack_command' + +class TestGemCommandsUnpackCommand < RubyGemTestCase + + def setup + super + + @cmd = Gem::Commands::UnpackCommand.new + end + + def test_execute + util_make_gems + + @cmd.options[:args] = %w[a] + + use_ui @ui do + Dir.chdir @tempdir do + @cmd.execute + end + end + + assert File.exist?(File.join(@tempdir, 'a-0.0.2')) + end + + def test_execute_exact_match + foo_spec = quick_gem 'foo' + foo_bar_spec = quick_gem 'foo_bar' + + use_ui @ui do + Dir.chdir @tempdir do + Gem::Builder.new(foo_spec).build + Gem::Builder.new(foo_bar_spec).build + end + end + + foo_path = File.join(@tempdir, "#{foo_spec.full_name}.gem") + foo_bar_path = File.join(@tempdir, "#{foo_bar_spec.full_name}.gem") + Gem::Installer.new(foo_path).install + Gem::Installer.new(foo_bar_path).install + + @cmd.options[:args] = %w[foo] + + use_ui @ui do + Dir.chdir @tempdir do + @cmd.execute + end + end + + assert File.exist?(File.join(@tempdir, foo_spec.full_name)) + end + +end + diff --git a/test/rubygems/test_gem_config_file.rb b/test/rubygems/test_gem_config_file.rb new file mode 100644 index 0000000000..e0360b0d6b --- /dev/null +++ b/test/rubygems/test_gem_config_file.rb @@ -0,0 +1,210 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/config_file' + +class TestGemConfigFile < RubyGemTestCase + + def setup + super + + @temp_conf = File.join @tempdir, '.gemrc' + + @cfg_args = %W[--config-file #{@temp_conf}] + util_config_file + end + + def test_initialize + assert_equal @temp_conf, @cfg.config_file_name + + assert_equal false, @cfg.backtrace + assert_equal true, @cfg.update_sources + assert_equal false, @cfg.benchmark + assert_equal Gem::ConfigFile::DEFAULT_BULK_THRESHOLD, @cfg.bulk_threshold + assert_equal true, @cfg.verbose + assert_equal %w[http://gems.example.com], Gem.sources + + File.open @temp_conf, 'w' do |fp| + fp.puts ":backtrace: true" + fp.puts ":update_sources: false" + fp.puts ":benchmark: true" + fp.puts ":bulk_threshold: 10" + fp.puts ":verbose: false" + fp.puts ":sources:" + fp.puts " - http://more-gems.example.com" + fp.puts "install: --wrappers" + end + + util_config_file + + assert_equal true, @cfg.backtrace + assert_equal true, @cfg.benchmark + assert_equal 10, @cfg.bulk_threshold + assert_equal false, @cfg.verbose + assert_equal false, @cfg.update_sources + assert_equal %w[http://more-gems.example.com], Gem.sources + assert_equal '--wrappers', @cfg[:install] + end + + def test_initialize_handle_arguments_config_file + util_config_file %W[--config-file #{@temp_conf}] + + assert_equal @temp_conf, @cfg.config_file_name + end + + def test_initialize_handle_arguments_config_file_equals + util_config_file %W[--config-file=#{@temp_conf}] + + assert_equal @temp_conf, @cfg.config_file_name + end + + def test_handle_arguments + args = %w[--backtrace --bunch --of --args here] + + @cfg.handle_arguments args + + assert_equal %w[--bunch --of --args here], @cfg.args + end + + def test_handle_arguments_backtrace + assert_equal false, @cfg.backtrace + + args = %w[--backtrace] + + @cfg.handle_arguments args + + assert_equal true, @cfg.backtrace + end + + def test_handle_arguments_benchmark + assert_equal false, @cfg.benchmark + + args = %w[--benchmark] + + @cfg.handle_arguments args + + assert_equal true, @cfg.benchmark + end + + def test_handle_arguments_debug + old_dollar_DEBUG = $DEBUG + assert_equal false, $DEBUG + + args = %w[--debug] + + @cfg.handle_arguments args + + assert_equal true, $DEBUG + ensure + $DEBUG = old_dollar_DEBUG + end + + def test_handle_arguments_override + File.open @temp_conf, 'w' do |fp| + fp.puts ":benchmark: false" + end + + util_config_file %W[--benchmark --config-file=#{@temp_conf}] + + assert_equal true, @cfg.benchmark + end + + def test_handle_arguments_traceback + assert_equal false, @cfg.backtrace + + args = %w[--traceback] + + @cfg.handle_arguments args + + assert_equal true, @cfg.backtrace + end + + def test_really_verbose + assert_equal false, @cfg.really_verbose + + @cfg.verbose = true + + assert_equal false, @cfg.really_verbose + + @cfg.verbose = 1 + + assert_equal true, @cfg.really_verbose + end + + def test_write + @cfg.backtrace = true + @cfg.benchmark = true + @cfg.update_sources = false + @cfg.bulk_threshold = 10 + @cfg.verbose = false + Gem.sources.replace %w[http://more-gems.example.com] + @cfg[:install] = '--wrappers' + + @cfg.write + + util_config_file + + # These should not be written out to the config file. + assert_equal false, @cfg.backtrace, 'backtrace' + assert_equal false, @cfg.benchmark, 'benchmark' + assert_equal Gem::ConfigFile::DEFAULT_BULK_THRESHOLD, @cfg.bulk_threshold, + 'bulk_threshold' + assert_equal true, @cfg.update_sources, 'update_sources' + assert_equal true, @cfg.verbose, 'verbose' + + assert_equal '--wrappers', @cfg[:install], 'install' + + # this should be written out to the config file. + assert_equal %w[http://more-gems.example.com], Gem.sources + end + + def test_write_from_hash + File.open @temp_conf, 'w' do |fp| + fp.puts ":backtrace: true" + fp.puts ":benchmark: true" + fp.puts ":bulk_threshold: 10" + fp.puts ":update_sources: false" + fp.puts ":verbose: false" + fp.puts ":sources:" + fp.puts " - http://more-gems.example.com" + fp.puts "install: --wrappers" + end + + util_config_file + + @cfg.backtrace = :junk + @cfg.benchmark = :junk + @cfg.update_sources = :junk + @cfg.bulk_threshold = 20 + @cfg.verbose = :junk + Gem.sources.replace %w[http://even-more-gems.example.com] + @cfg[:install] = '--wrappers --no-rdoc' + + @cfg.write + + util_config_file + + # These should not be written out to the config file + assert_equal true, @cfg.backtrace, 'backtrace' + assert_equal true, @cfg.benchmark, 'benchmark' + assert_equal 10, @cfg.bulk_threshold, 'bulk_threshold' + assert_equal false, @cfg.update_sources, 'update_sources' + assert_equal false, @cfg.verbose, 'verbose' + + assert_equal '--wrappers --no-rdoc', @cfg[:install], 'install' + + assert_equal %w[http://even-more-gems.example.com], Gem.sources + end + + def util_config_file(args = @cfg_args) + @cfg = Gem::ConfigFile.new args + end + +end + diff --git a/test/rubygems/test_gem_dependency.rb b/test/rubygems/test_gem_dependency.rb new file mode 100644 index 0000000000..f280221a00 --- /dev/null +++ b/test/rubygems/test_gem_dependency.rb @@ -0,0 +1,89 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/version' + +class TestGemDependency < RubyGemTestCase + + def setup + super + + @pkg1_0 = Gem::Dependency.new 'pkg', ['> 1.0'] + @pkg1_1 = Gem::Dependency.new 'pkg', ['> 1.1'] + + @oth1_0 = Gem::Dependency.new 'other', ['> 1.0'] + + @r1_0 = Gem::Requirement.new ['> 1.0'] + end + + def test_initialize + assert_equal "pkg", @pkg1_0.name + assert_equal @r1_0, @pkg1_0.version_requirements + end + + def test_initialize_double + dep = Gem::Dependency.new("pkg", ["> 1.0", "< 2.0"]) + + assert_equal Gem::Requirement.new(["> 1.0", "< 2.0"]), + dep.version_requirements + end + + def test_initialize_empty + dep = Gem::Dependency.new("pkg", []) + req = @r1_0 + + req.instance_eval do + @version = ">= 1.0" + @op = ">=" + @nums = [1,0] + @requirements = nil + end + + dep.instance_eval do + @version_requirement = req + @version_requirements = nil + end + + assert_equal Gem::Requirement.new([">= 1.0"]), dep.version_requirements + end + + def test_initialize_version + dep = Gem::Dependency.new 'pkg', Gem::Version.new('2') + + assert_equal 'pkg', dep.name + + assert_equal Gem::Requirement.new('= 2'), dep.version_requirements + end + + def test_equals2 + assert_equal @pkg1_0, @pkg1_0.dup + assert_equal @pkg1_0.dup, @pkg1_0 + + assert_not_equal @pkg1_0, @pkg1_1, "requirements different" + assert_not_equal @pkg1_1, @pkg1_0, "requirements different" + + assert_not_equal @pkg1_0, @oth1_0, "names different" + assert_not_equal @oth1_0, @pkg1_0, "names different" + + assert_not_equal @pkg1_0, Object.new + assert_not_equal Object.new, @pkg1_0 + end + + def test_hash + assert_equal @pkg1_0.hash, @pkg1_0.dup.hash + assert_equal @pkg1_0.dup.hash, @pkg1_0.hash + + assert_not_equal @pkg1_0.hash, @pkg1_1.hash, "requirements different" + assert_not_equal @pkg1_1.hash, @pkg1_0.hash, "requirements different" + + assert_not_equal @pkg1_0.hash, @oth1_0.hash, "names different" + assert_not_equal @oth1_0.hash, @pkg1_0.hash, "names different" + end + +end + diff --git a/test/rubygems/test_gem_dependency_installer.rb b/test/rubygems/test_gem_dependency_installer.rb new file mode 100644 index 0000000000..18793d3065 --- /dev/null +++ b/test/rubygems/test_gem_dependency_installer.rb @@ -0,0 +1,519 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/dependency_installer' + +class TestGemDependencyInstaller < RubyGemTestCase + + def setup + super + + @gems_dir = File.join @tempdir, 'gems' + @cache_dir = File.join @gemhome, 'cache' + FileUtils.mkdir @gems_dir + + write_file File.join('gems', 'a-1', 'bin', 'a_bin') do |fp| + fp.puts "#!/usr/bin/ruby" + end + @a1, @a1_gem = util_gem 'a', '1' do |s| s.executables << 'a_bin' end + + @b1, @b1_gem = util_gem 'b', '1' do |s| s.add_dependency 'a' end + + @d1, @d1_gem = util_gem 'd', '1' + @d2, @d2_gem = util_gem 'd', '2' + + @x1_m, @x1_m_gem = util_gem 'x', '1' do |s| + s.platform = Gem::Platform.new %w[cpu my_platform 1] + end + + @x1_o, @x1_o_gem = util_gem 'x', '1' do |s| + s.platform = Gem::Platform.new %w[cpu other_platform 1] + end + + @w1, @w1_gem = util_gem 'w', '1' do |s| s.add_dependency 'x' end + + @y1, @y1_gem = util_gem 'y', '1' + @y1_1_p, @y1_1_p_gem = util_gem 'y', '1.1' do |s| + s.platform = Gem::Platform.new %w[cpu my_platform 1] + end + + @z1, @z1_gem = util_gem 'z', '1' do |s| s.add_dependency 'y' end + + si = util_setup_source_info_cache @a1, @b1, @d1, @d2, @x1_m, @x1_o, @w1, + @y1, @y1_1_p, @z1 + + @fetcher = FakeFetcher.new + Gem::RemoteFetcher.instance_variable_set :@fetcher, @fetcher + @fetcher.uri = URI.parse 'http://gems.example.com' + @fetcher.data['http://gems.example.com/gems/yaml'] = si.to_yaml + + FileUtils.rm_rf File.join(@gemhome, 'gems') + end + + def test_install + FileUtils.mv @a1_gem, @tempdir + inst = nil + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new 'a' + inst.install + end + + assert_equal Gem::SourceIndex.new(@a1.full_name => @a1), + Gem::SourceIndex.from_installed_gems + + assert_equal [@a1], inst.installed_gems + end + + def test_install_dependency + FileUtils.mv @a1_gem, @tempdir + FileUtils.mv @b1_gem, @tempdir + inst = nil + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new 'b' + inst.install + end + + assert_equal %w[a-1 b-1], inst.installed_gems.map { |s| s.full_name } + end + + def test_install_dependency_existing + Gem::Installer.new(@a1_gem).install + FileUtils.mv @a1_gem, @tempdir + FileUtils.mv @b1_gem, @tempdir + inst = nil + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new 'b' + inst.install + end + + assert_equal %w[b-1], inst.installed_gems.map { |s| s.full_name } + end + + def test_install_dependency_old + @e1, @e1_gem = util_gem 'e', '1' + @f1, @f1_gem = util_gem 'f', '1' do |s| s.add_dependency 'e' end + @f2, @f2_gem = util_gem 'f', '2' + + FileUtils.mv @e1_gem, @tempdir + FileUtils.mv @f1_gem, @tempdir + FileUtils.mv @f2_gem, @tempdir + inst = nil + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new 'f' + inst.install + end + + assert_equal %w[f-2], inst.installed_gems.map { |s| s.full_name } + end + + def test_install_local + FileUtils.mv @a1_gem, @tempdir + inst = nil + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new 'a-1.gem' + inst.install + end + + assert_equal %w[a-1], inst.installed_gems.map { |s| s.full_name } + end + + def test_install_local_subdir + inst = nil + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new 'gems/a-1.gem' + inst.install + end + + assert_equal %w[a-1], inst.installed_gems.map { |s| s.full_name } + end + + def test_install_env_shebang + FileUtils.mv @a1_gem, @tempdir + inst = nil + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new 'a', nil, :env_shebang => true, + :wrappers => true + inst.install + end + + assert_match %r|\A#!/usr/bin/env ruby\n|, + File.read(File.join(@gemhome, 'bin', 'a_bin')) + end + + def test_install_force + FileUtils.mv @b1_gem, @tempdir + si = util_setup_source_info_cache @b1 + @fetcher.data['http://gems.example.com/gems/yaml'] = si.to_yaml + inst = nil + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new 'b', nil, :force => true + inst.install + end + + assert_equal %w[b-1], inst.installed_gems.map { |s| s.full_name } + end + + def test_install_ignore_dependencies + FileUtils.mv @b1_gem, @tempdir + inst = nil + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new 'b', nil, :ignore_dependencies => true + inst.install + end + + assert_equal %w[b-1], inst.installed_gems.map { |s| s.full_name } + end + + def test_install_install_dir + FileUtils.mv @a1_gem, @tempdir + gemhome2 = File.join @tempdir, 'gemhome2' + Dir.mkdir gemhome2 + inst = nil + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new 'a', nil, :install_dir => gemhome2 + inst.install + end + + assert_equal %w[a-1], inst.installed_gems.map { |s| s.full_name } + + assert File.exist?(File.join(gemhome2, 'specifications', + "#{@a1.full_name}.gemspec")) + end + + def test_install_domain_both + a1_data = nil + File.open @a1_gem, 'rb' do |fp| + a1_data = fp.read + end + + @fetcher.data['http://gems.example.com/gems/a-1.gem'] = a1_data + + FileUtils.mv @b1_gem, @tempdir + inst = nil + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new 'b', nil, :domain => :both + inst.install + end + + assert_equal %w[a-1 b-1], inst.installed_gems.map { |s| s.full_name } + a1, b1 = inst.installed_gems + + a1_expected = File.join(@gemhome, 'specifications', + "#{a1.full_name}.gemspec") + b1_expected = File.join(@gemhome, 'specifications', + "#{b1.full_name}.gemspec") + + assert_equal a1_expected, a1.loaded_from + assert_equal b1_expected, b1.loaded_from + end + + def test_install_domain_local + FileUtils.mv @b1_gem, @tempdir + inst = nil + + Dir.chdir @tempdir do + e = assert_raise Gem::InstallError do + inst = Gem::DependencyInstaller.new 'b', nil, :domain => :local + inst.install + end + assert_equal 'b requires a (>= 0)', e.message + end + + assert_equal [], inst.installed_gems.map { |s| s.full_name } + end + + def test_install_domain_remote + a1_data = nil + File.open @a1_gem, 'rb' do |fp| + a1_data = fp.read + end + + @fetcher.data['http://gems.example.com/gems/a-1.gem'] = a1_data + + inst = Gem::DependencyInstaller.new 'a', nil, :domain => :remote + inst.install + + assert_equal %w[a-1], inst.installed_gems.map { |s| s.full_name } + end + + def test_install_domain_remote_platform_newer + a2_o, a2_o_gem = util_gem 'a', '2' do |s| + s.platform = Gem::Platform.new %w[cpu other_platform 1] + end + + si = util_setup_source_info_cache @a1, a2_o + + @fetcher.data['http://gems.example.com/gems/yaml'] = si.to_yaml + + a1_data = nil + a2_o_data = nil + + File.open @a1_gem, 'rb' do |fp| a1_data = fp.read end + File.open a2_o_gem, 'rb' do |fp| a2_o_data = fp.read end + + @fetcher.data["http://gems.example.com/gems/#{@a1.full_name}.gem"] = + a1_data + @fetcher.data["http://gems.example.com/gems/#{a2_o.full_name}.gem"] = + a2_o_data + + inst = Gem::DependencyInstaller.new 'a', nil, :domain => :remote + inst.install + + assert_equal %w[a-1], inst.installed_gems.map { |s| s.full_name } + end + + def test_install_reinstall + Gem::Installer.new(@a1_gem).install + FileUtils.mv @a1_gem, @tempdir + inst = nil + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new 'a' + inst.install + end + + assert_equal Gem::SourceIndex.new(@a1.full_name => @a1), + Gem::SourceIndex.from_installed_gems + + assert_equal %w[a-1], inst.installed_gems.map { |s| s.full_name } + end + + def test_install_security_policy + FileUtils.mv @a1_gem, @cache_dir + FileUtils.mv @b1_gem, @cache_dir + policy = Gem::Security::HighSecurity + inst = Gem::DependencyInstaller.new 'b', nil, :security_policy => policy + + e = assert_raise Gem::Exception do + inst.install + end + + assert_equal 'Unsigned gem', e.message + + assert_equal %w[], inst.installed_gems.map { |s| s.full_name } + end + + def test_install_wrappers + FileUtils.mv @a1_gem, @cache_dir + inst = Gem::DependencyInstaller.new 'a', :wrappers => true + + inst.install + + assert_match %r|This file was generated by RubyGems.|, + File.read(File.join(@gemhome, 'bin', 'a_bin')) + end + + def test_install_version + FileUtils.mv @d1_gem, @cache_dir + FileUtils.mv @d2_gem, @cache_dir + inst = Gem::DependencyInstaller.new 'd', '= 1' + + inst.install + + assert_equal %w[d-1], inst.installed_gems.map { |s| s.full_name } + end + + def test_install_version_default + FileUtils.mv @d1_gem, @cache_dir + FileUtils.mv @d2_gem, @cache_dir + inst = Gem::DependencyInstaller.new 'd' + + inst.install + + assert_equal %w[d-2], inst.installed_gems.map { |s| s.full_name } + end + + def test_download_gem + a1_data = nil + File.open @a1_gem, 'rb' do |fp| + a1_data = fp.read + end + + @fetcher.data['http://gems.example.com/gems/a-1.gem'] = a1_data + + inst = Gem::DependencyInstaller.new 'a' + + a1_cache_gem = File.join(@gemhome, 'cache', "#{@a1.full_name}.gem") + assert_equal a1_cache_gem, inst.download(@a1, 'http://gems.example.com') + + assert File.exist?(a1_cache_gem) + end + + def test_download_gem_cached + FileUtils.mv @a1_gem, @cache_dir + + inst = Gem::DependencyInstaller.new 'a' + + assert_equal File.join(@gemhome, 'cache', "#{@a1.full_name}.gem"), + inst.download(@a1, 'http://gems.example.com') + end + + def test_download_gem_local + FileUtils.mv @a1_gem, @tempdir + local_path = File.join @tempdir, "#{@a1.full_name}.gem" + inst = nil + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new 'a' + end + + assert_equal File.join(@gemhome, 'cache', "#{@a1.full_name}.gem"), + inst.download(@a1, local_path) + end + + def test_download_gem_install_dir + a1_data = nil + File.open @a1_gem, 'rb' do |fp| + a1_data = fp.read + end + + @fetcher.data['http://gems.example.com/gems/a-1.gem'] = a1_data + + install_dir = File.join @tempdir, 'more_gems' + + inst = Gem::DependencyInstaller.new 'a', nil, :install_dir => install_dir + + a1_cache_gem = File.join install_dir, 'cache', "#{@a1.full_name}.gem" + assert_equal a1_cache_gem, inst.download(@a1, 'http://gems.example.com') + + assert File.exist?(a1_cache_gem) + end + + unless win_platform? then # File.chmod doesn't work + def test_download_gem_local_read_only + FileUtils.mv @a1_gem, @tempdir + local_path = File.join @tempdir, "#{@a1.full_name}.gem" + inst = nil + File.chmod 0555, File.join(@gemhome, 'cache') + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new 'a' + end + + assert_equal File.join(@tempdir, "#{@a1.full_name}.gem"), + inst.download(@a1, local_path) + ensure + File.chmod 0755, File.join(@gemhome, 'cache') + end + end + + def test_download_gem_unsupported + inst = Gem::DependencyInstaller.new 'a' + + e = assert_raise Gem::InstallError do + inst.download @a1, 'ftp://gems.rubyforge.org' + end + + assert_equal 'unsupported URI scheme ftp', e.message + end + + def test_find_gems_gems_with_sources + inst = Gem::DependencyInstaller.new 'a' + dep = Gem::Dependency.new 'b', '>= 0' + + assert_equal [[@b1, 'http://gems.example.com']], + inst.find_gems_with_sources(dep) + end + + def test_find_gems_with_sources_local + FileUtils.mv @a1_gem, @tempdir + inst = Gem::DependencyInstaller.new 'b' + dep = Gem::Dependency.new 'a', '>= 0' + gems = nil + + Dir.chdir @tempdir do + gems = inst.find_gems_with_sources dep + end + + assert_equal 2, gems.length + remote = gems.first + assert_equal @a1, remote.first, 'remote spec' + assert_equal 'http://gems.example.com', remote.last, 'remote path' + + local = gems.last + assert_equal 'a-1', local.first.full_name, 'local spec' + assert_equal File.join(@tempdir, "#{@a1.full_name}.gem"), + local.last, 'local path' + end + + def test_gather_dependencies + inst = Gem::DependencyInstaller.new 'b' + + assert_equal %w[a-1 b-1], inst.gems_to_install.map { |s| s.full_name } + end + + def test_gather_dependencies_dropped + b2, = util_gem 'b', '2' + c1, = util_gem 'c', '1' do |s| s.add_dependency 'b' end + + si = util_setup_source_info_cache @a1, @b1, b2, c1 + + @fetcher = FakeFetcher.new + Gem::RemoteFetcher.instance_variable_set :@fetcher, @fetcher + @fetcher.uri = URI.parse 'http://gems.example.com' + @fetcher.data['http://gems.example.com/gems/yaml'] = si.to_yaml + + inst = Gem::DependencyInstaller.new 'c' + + assert_equal %w[b-2 c-1], inst.gems_to_install.map { |s| s.full_name } + end + + def test_gather_dependencies_platform_alternate + util_set_arch 'cpu-my_platform1' + + inst = Gem::DependencyInstaller.new 'w' + + assert_equal %w[x-1-cpu-my_platform-1 w-1], + inst.gems_to_install.map { |s| s.full_name } + end + + def test_gather_dependencies_platform_bump + inst = Gem::DependencyInstaller.new 'z' + + assert_equal %w[y-1 z-1], inst.gems_to_install.map { |s| s.full_name } + end + + def test_gather_dependencies_old_required + e1, = util_gem 'e', '1' do |s| s.add_dependency 'd', '= 1' end + + si = util_setup_source_info_cache @d1, @d2, e1 + + @fetcher = FakeFetcher.new + Gem::RemoteFetcher.instance_variable_set :@fetcher, @fetcher + @fetcher.uri = URI.parse 'http://gems.example.com' + @fetcher.data['http://gems.example.com/gems/yaml'] = si.to_yaml + + inst = Gem::DependencyInstaller.new 'e' + + assert_equal %w[d-1 e-1], inst.gems_to_install.map { |s| s.full_name } + end + + def util_gem(name, version, &block) + spec = quick_gem(name, version, &block) + + util_build_gem spec + + cache_file = File.join @tempdir, 'gems', "#{spec.full_name}.gem" + FileUtils.mv File.join(@gemhome, 'cache', "#{spec.full_name}.gem"), + cache_file + FileUtils.rm File.join(@gemhome, 'specifications', + "#{spec.full_name}.gemspec") + + spec.loaded_from = nil + spec.loaded = false + + [spec, cache_file] + end + +end + diff --git a/test/rubygems/test_gem_dependency_list.rb b/test/rubygems/test_gem_dependency_list.rb new file mode 100644 index 0000000000..5fdc227f05 --- /dev/null +++ b/test/rubygems/test_gem_dependency_list.rb @@ -0,0 +1,212 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/dependency_list' + +class TestGemDependencyList < RubyGemTestCase + + def setup + super + + @deplist = Gem::DependencyList.new + + @a1 = quick_gem 'a', '1' + @a2 = quick_gem 'a', '2' + @a3 = quick_gem 'a', '3' + + @b1 = quick_gem 'b', '1' do |s| s.add_dependency 'a', '>= 1' end + @b2 = quick_gem 'b', '2' do |s| s.add_dependency 'a', '>= 1' end + + @c1 = quick_gem 'c', '1' do |s| s.add_dependency 'b', '>= 1' end + @c2 = quick_gem 'c', '2' + + @d1 = quick_gem 'd', '1' do |s| s.add_dependency 'c', '>= 1' end + end + + def test_self_from_source_index + hash = { + 'a-1' => @a1, + 'b-2' => @b2, + } + + si = Gem::SourceIndex.new hash + deps = Gem::DependencyList.from_source_index si + + assert_equal %w[b-2 a-1], deps.dependency_order.map { |s| s.full_name } + end + + def test_active_count + assert_equal 0, @deplist.send(:active_count, [], {}) + assert_equal 1, @deplist.send(:active_count, [@a1], {}) + assert_equal 0, @deplist.send(:active_count, [@a1], + { @a1.full_name => true }) + end + + def test_add + assert_equal [], @deplist.dependency_order + + @deplist.add @a1, @b2 + + assert_equal [@b2, @a1], @deplist.dependency_order + end + + def test_dependency_order + @deplist.add @a1, @b1, @c1, @d1 + + order = @deplist.dependency_order + + assert_equal %w[d-1 c-1 b-1 a-1], order.map { |s| s.full_name } + end + + def test_dependency_order_circle + @a1.add_dependency 'c', '>= 1' + @deplist.add @a1, @b1, @c1 + + order = @deplist.dependency_order + + assert_equal %w[b-1 c-1 a-1], order.map { |s| s.full_name } + end + + def test_dependency_order_diamond + util_diamond + e1 = quick_gem 'e', '1' + @deplist.add e1 + @a1.add_dependency 'e', '>= 1' + + order = @deplist.dependency_order + + assert_equal %w[d-1 c-2 b-1 a-2 e-1], order.map { |s| s.full_name }, + 'deps of trimmed specs not included' + end + + def test_dependency_order_no_dependendencies + @deplist.add @a1, @c2 + + order = @deplist.dependency_order + + assert_equal %w[c-2 a-1], order.map { |s| s.full_name } + end + + def test_find_name + @deplist.add @a1, @b2 + + assert_equal "a-1", @deplist.find_name("a-1").full_name + assert_equal "b-2", @deplist.find_name("b-2").full_name + + assert_nil @deplist.find_name("c-2") + end + + def test_ok_eh + assert @deplist.ok?, 'no dependencies' + + @deplist.add @b2 + + assert ! @deplist.ok?, 'unsatisfied dependency' + + @deplist.add @a1 + + assert @deplist.ok?, 'satisfied dependency' + end + + def test_ok_eh_mismatch + a1 = quick_gem 'a', '1' + a2 = quick_gem 'a', '2' + + b = quick_gem 'b', '1' do |s| s.add_dependency 'a', '= 1' end + c = quick_gem 'c', '1' do |s| s.add_dependency 'a', '= 2' end + + d = quick_gem 'd', '1' do |s| + s.add_dependency 'b' + s.add_dependency 'c' + end + + @deplist.add a1, a2, b, c, d + + assert @deplist.ok?, 'this will break on require' + end + + def test_ok_eh_redundant + @deplist.add @a1, @a3, @b2 + + @deplist.remove_by_name("a-1") + + assert @deplist.ok? + end + + def test_ok_to_remove_eh + @deplist.add @a1 + + assert @deplist.ok_to_remove?("a-1") + + @deplist.add @b2 + + assert ! @deplist.ok_to_remove?("a-1") + + @deplist.add @a2 + + assert @deplist.ok_to_remove?("a-1") + assert @deplist.ok_to_remove?("a-2") + assert @deplist.ok_to_remove?("b-2") + end + + def test_ok_to_remove_eh_after_sibling_removed + @deplist.add @a1, @a2, @b2 + + assert @deplist.ok_to_remove?("a-1") + assert @deplist.ok_to_remove?("a-2") + + @deplist.remove_by_name("a-1") + + assert ! @deplist.ok_to_remove?("a-2") + end + + def test_remove_by_name + @deplist.add @a1, @b2 + + @deplist.remove_by_name "a-1" + + assert ! @deplist.ok? + end + + def test_tsort_each_node + util_diamond + + order = %w[a-1 a-2 b-1 c-2 d-1] + + @deplist.tsort_each_node do |node| + assert_equal order.shift, node.full_name + end + + assert order.empty? + end + + def test_tsort_each_child + util_diamond + + order = %w[a-2] + + @deplist.tsort_each_child(@b1) do |node| + assert_equal order.shift, node.full_name + end + + assert order.empty? + end + + # d1 -> b1 -> a1 + # d1 -> c2 -> a2 + def util_diamond + @c2.add_dependency 'a', '>= 2' + @d1.add_dependency 'b' + + @deplist.add @a1, @a2, @b1, @c2, @d1 + end + +end + diff --git a/test/rubygems/test_gem_digest.rb b/test/rubygems/test_gem_digest.rb new file mode 100755 index 0000000000..9d825b2796 --- /dev/null +++ b/test/rubygems/test_gem_digest.rb @@ -0,0 +1,44 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require "test/unit" +require "rubygems/digest/md5" +require "rubygems/digest/sha1" +require "rubygems/digest/sha2" + +class TestRubygemsGemDigest < Test::Unit::TestCase + def test_sha256_hex_digest_works + digester = Gem::SHA256.new + assert_equal "b5d4045c3f466fa91fe2cc6abe79232a1a57cdf104f7a26e716e0a1e2789df78", digester.hexdigest("ABC") + end + + def test_sha256_digest_works + digester = Gem::SHA256.new + assert_equal "\265\324\004\\?Fo\251\037\342\314j\276y#*\032W\315\361\004\367\242nqn\n\036'\211\337x", + digester.digest("ABC") + end + + def test_sha1_hex_digest_works + digester = Gem::SHA1.new + assert_equal "3c01bdbb26f358bab27f267924aa2c9a03fcfdb8", digester.hexdigest("ABC") + end + + def test_sha1_digest_works + digester = Gem::SHA1.new + assert_equal "<\001\275\273&\363X\272\262\177&y$\252,\232\003\374\375\270", digester.digest("ABC") + end + + def test_md5_hex_digest_works + digester = Gem::MD5.new + assert_equal "902fbdd2b1df0c4f70b4a5d23525e932", digester.hexdigest("ABC") + end + + def test_md5_digest_works + digester = Gem::MD5.new + assert_equal "\220/\275\322\261\337\fOp\264\245\3225%\3512", digester.digest("ABC") + end +end \ No newline at end of file diff --git a/test/rubygems/test_gem_doc_manager.rb b/test/rubygems/test_gem_doc_manager.rb new file mode 100644 index 0000000000..e52fb9f0cd --- /dev/null +++ b/test/rubygems/test_gem_doc_manager.rb @@ -0,0 +1,32 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/doc_manager' + +class TestGemDocManager < RubyGemTestCase + + def setup + super + + @spec = quick_gem 'a' + @manager = Gem::DocManager.new(@spec) + end + + def test_uninstall_doc_unwritable + orig_mode = File.stat(@spec.installation_path).mode + File.chmod 0, @spec.installation_path + + assert_raise Gem::FilePermissionError do + @manager.uninstall_doc + end + ensure + File.chmod orig_mode, @spec.installation_path + end + +end + diff --git a/test/rubygems/test_gem_ext_configure_builder.rb b/test/rubygems/test_gem_ext_configure_builder.rb new file mode 100644 index 0000000000..c32aa2e23f --- /dev/null +++ b/test/rubygems/test_gem_ext_configure_builder.rb @@ -0,0 +1,84 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/ext' + +class TestGemExtConfigureBuilder < RubyGemTestCase + + def setup + super + + @makefile_body = "all:\n\t@echo ok\ninstall:\n\t@echo ok" + + @ext = File.join @tempdir, 'ext' + @dest_path = File.join @tempdir, 'prefix' + + FileUtils.mkdir_p @ext + FileUtils.mkdir_p @dest_path + end + + def test_self_build + return if RUBY_PLATFORM =~ /mswin/ # HACK + + File.open File.join(@ext, './configure'), 'w' do |configure| + configure.puts "#!/bin/sh\necho \"#{@makefile_body}\" > Makefile" + end + + output = [] + + Dir.chdir @ext do + Gem::Ext::ConfigureBuilder.build nil, nil, @dest_path, output + end + + expected = [ + "sh ./configure --prefix=#{@dest_path}", + "", "make", "ok\n", "make install", "ok\n" + ] + + assert_equal expected, output + end + + def test_self_build_fail + return if RUBY_PLATFORM =~ /mswin/ # HACK + output = [] + + error = assert_raise Gem::InstallError do + Dir.chdir @ext do + Gem::Ext::ConfigureBuilder.build nil, nil, @dest_path, output + end + end + + expected = %r|configure failed: + +sh \./configure --prefix=#{@dest_path} +.*?: \./configure: No such file or directory +| + + assert_match expected, error.message + + assert_equal "sh ./configure --prefix=#{@dest_path}", output.shift + assert_match %r|\./configure: No such file or directory\n|, output.shift + assert_equal true, output.empty? + end + + def test_self_build_has_makefile + File.open File.join(@ext, 'Makefile'), 'w' do |makefile| + makefile.puts @makefile_body + end + + output = [] + Dir.chdir @ext do + Gem::Ext::ConfigureBuilder.build nil, nil, @dest_path, output + end + + case RUBY_PLATFORM + when /mswin/ then + assert_equal 'nmake', output[0] + assert_equal 'nmake install', output[2] + else + assert_equal 'make', output[0] + assert_equal 'make install', output[2] + end + end + +end + diff --git a/test/rubygems/test_gem_ext_ext_conf_builder.rb b/test/rubygems/test_gem_ext_ext_conf_builder.rb new file mode 100644 index 0000000000..fb21fa0755 --- /dev/null +++ b/test/rubygems/test_gem_ext_ext_conf_builder.rb @@ -0,0 +1,122 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/ext' + +class TestGemExtExtConfBuilder < RubyGemTestCase + + def setup + super + + @ext = File.join @tempdir, 'ext' + @dest_path = File.join @tempdir, 'prefix' + + FileUtils.mkdir_p @ext + FileUtils.mkdir_p @dest_path + end + + def test_class_build + File.open File.join(@ext, 'extconf.rb'), 'w' do |extconf| + extconf.puts "require 'mkmf'\ncreate_makefile 'foo'" + end + + output = [] + + Dir.chdir @ext do + Gem::Ext::ExtConfBuilder.build 'extconf.rb', nil, @dest_path, output + end + + expected = [ + "ruby extconf.rb", + "creating Makefile\n", + "make", + "make: Nothing to be done for `all'.\n", + "make install", + "make: Nothing to be done for `install'.\n" + ] + + assert_match(/^#{Gem.ruby} extconf.rb/, output[0]) + assert_equal "creating Makefile\n", output[1] + case RUBY_PLATFORM + when /mswin/ then + assert_equal "nmake", output[2] + assert_equal "nmake install", output[4] + else + assert_equal "make", output[2] + assert_equal "make install", output[4] + end + end + + def test_class_build_extconf_fail + File.open File.join(@ext, 'extconf.rb'), 'w' do |extconf| + extconf.puts "require 'mkmf'" + extconf.puts "have_library 'nonexistent' or abort 'need libnonexistent'" + extconf.puts "create_makefile 'foo'" + end + + output = [] + + error = assert_raise Gem::InstallError do + Dir.chdir @ext do + Gem::Ext::ExtConfBuilder.build 'extconf.rb', nil, @dest_path, output + end + end + + assert_match(/\Aextconf failed: + +#{Gem.ruby} extconf.rb.* +checking for main\(\) in .*?nonexistent/m, error.message) + + assert_match(/^#{Gem.ruby} extconf.rb/, output[0]) + end + + def test_class_make + output = [] + makefile_path = File.join(@ext, 'Makefile') + File.open makefile_path, 'w' do |makefile| + makefile.puts "RUBYARCHDIR = $(foo)$(target_prefix)" + makefile.puts "RUBYLIBDIR = $(bar)$(target_prefix)" + makefile.puts "all:" + makefile.puts "install:" + end + + Dir.chdir @ext do + Gem::Ext::ExtConfBuilder.make @ext, output + end + + case RUBY_PLATFORM + when /mswin/ then + assert_equal 'nmake', output[0] + assert_equal 'nmake install', output[2] + else + assert_equal 'make', output[0] + assert_equal 'make install', output[2] + end + + edited_makefile = <<-EOF +RUBYARCHDIR = #{@ext}$(target_prefix) +RUBYLIBDIR = #{@ext}$(target_prefix) +all: +install: + EOF + + assert_equal edited_makefile, File.read(makefile_path) + end + + def test_class_make_no_Makefile + error = assert_raise Gem::InstallError do + Dir.chdir @ext do + Gem::Ext::ExtConfBuilder.make @ext, ['output'] + end + end + + expected = <<-EOF.strip +Makefile not found: + +output + EOF + + assert_equal expected, error.message + end + +end + diff --git a/test/rubygems/test_gem_ext_rake_builder.rb b/test/rubygems/test_gem_ext_rake_builder.rb new file mode 100644 index 0000000000..cd63106077 --- /dev/null +++ b/test/rubygems/test_gem_ext_rake_builder.rb @@ -0,0 +1,73 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/ext' + +class TestGemExtRakeBuilder < RubyGemTestCase + + def setup + super + + @ext = File.join @tempdir, 'ext' + @dest_path = File.join @tempdir, 'prefix' + + FileUtils.mkdir_p @ext + FileUtils.mkdir_p @dest_path + end + + def test_class_build + File.open File.join(@ext, 'mkrf_conf.rb'), 'w' do |mkrf_conf| + mkrf_conf.puts <<-EO_MKRF + File.open("Rakefile","w") do |f| + f.puts "task :default" + end + EO_MKRF + end + + output = [] + realdir = nil # HACK /tmp vs. /private/tmp + + Dir.chdir @ext do + realdir = Dir.pwd + Gem::Ext::RakeBuilder.build 'mkrf_conf.rb', nil, @dest_path, output + end + + expected = [ + "#{Gem.ruby} mkrf_conf.rb", + "", + "rake RUBYARCHDIR=#{@dest_path} RUBYLIBDIR=#{@dest_path}", + "(in #{realdir})\n" + ] + + assert_equal expected, output + end + + def test_class_build_fail + File.open File.join(@ext, 'mkrf_conf.rb'), 'w' do |mkrf_conf| + mkrf_conf.puts <<-EO_MKRF + File.open("Rakefile","w") do |f| + f.puts "task :default do abort 'fail' end" + end + EO_MKRF + end + + output = [] + + error = assert_raise Gem::InstallError do + Dir.chdir @ext do + Gem::Ext::RakeBuilder.build "mkrf_conf.rb", nil, @dest_path, output + end + end + + expected = <<-EOF.strip +rake failed: + +#{Gem.ruby} mkrf_conf.rb + +rake RUBYARCHDIR=#{@dest_path} RUBYLIBDIR=#{@dest_path} + EOF + + assert_equal expected, error.message.split("\n")[0..4].join("\n") + end + +end + diff --git a/test/rubygems/test_gem_format.rb b/test/rubygems/test_gem_format.rb new file mode 100644 index 0000000000..2b7d821952 --- /dev/null +++ b/test/rubygems/test_gem_format.rb @@ -0,0 +1,51 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require File.join(File.expand_path(File.dirname(__FILE__)), 'simple_gem') +require 'rubygems/format' + +class TestGemFormat < RubyGemTestCase + + def setup + super + + @simple_gem = SIMPLE_GEM + end + + def test_from_file_by_path_nonexistent + assert_raise Gem::Exception do + Gem::Format.from_file_by_path '/nonexistent' + end + end + + def test_from_io_garbled + e = assert_raise Gem::Package::FormatError do + # subtly bogus input + Gem::Format.from_io(StringIO.new(@simple_gem.upcase)) + end + + assert_equal 'No metadata found!', e.message + + e = assert_raise Gem::Package::FormatError do + # Totally bogus input + Gem::Format.from_io(StringIO.new(@simple_gem.reverse)) + end + + assert_equal 'No metadata found!', e.message + + e = assert_raise Gem::Package::FormatError do + # This was intentionally screws up YAML parsing. + Gem::Format.from_io(StringIO.new(@simple_gem.gsub(/:/, "boom"))) + end + + assert_equal 'No metadata found!', e.message + end + +end + + diff --git a/test/rubygems/test_gem_gem_path_searcher.rb b/test/rubygems/test_gem_gem_path_searcher.rb new file mode 100644 index 0000000000..d35416e867 --- /dev/null +++ b/test/rubygems/test_gem_gem_path_searcher.rb @@ -0,0 +1,57 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/gem_path_searcher' + +class Gem::GemPathSearcher + attr_accessor :gemspecs + attr_accessor :lib_dirs + + public :init_gemspecs + public :matching_file + public :lib_dirs_for +end + +class TestGemGemPathSearcher < RubyGemTestCase + + def setup + super + + @foo1 = quick_gem 'foo', '0.1' do |s| + s.require_paths << 'lib2' + s.files << 'lib/foo.rb' + end + + path = File.join 'gems', @foo1.full_name, 'lib', 'foo.rb' + write_file(path) { |fp| fp.puts "# #{path}" } + + @foo2 = quick_gem 'foo', '0.2' + @bar1 = quick_gem 'bar', '0.1' + @bar2 = quick_gem 'bar', '0.2' + + Gem.source_index = util_setup_source_info_cache @foo1, @foo2, @bar1, @bar2 + + @gps = Gem::GemPathSearcher.new + end + + def test_find + assert_equal @foo1, @gps.find('foo') + end + + def test_init_gemspecs + assert_equal [@bar2, @bar1, @foo2, @foo1], @gps.init_gemspecs + end + + def test_lib_dirs_for + lib_dirs = @gps.lib_dirs_for(@foo1) + expected = File.join @gemhome, 'gems', @foo1.full_name, '{lib,lib2}' + + assert_equal expected, lib_dirs + end + + def test_matching_file + assert !@gps.matching_file(@foo1, 'bar') + assert @gps.matching_file(@foo1, 'foo') + end + +end + diff --git a/test/rubygems/test_gem_gem_runner.rb b/test/rubygems/test_gem_gem_runner.rb new file mode 100644 index 0000000000..4e3239f015 --- /dev/null +++ b/test/rubygems/test_gem_gem_runner.rb @@ -0,0 +1,35 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/gem_runner' + +class TestGemGemRunner < RubyGemTestCase + + def test_do_configuration + Gem.clear_paths + + temp_conf = File.join @tempdir, '.gemrc' + + other_gem_path = File.join @tempdir, 'other_gem_path' + other_gem_home = File.join @tempdir, 'other_gem_home' + + Gem.ensure_gem_subdirectories other_gem_path + Gem.ensure_gem_subdirectories other_gem_home + + File.open temp_conf, 'w' do |fp| + fp.puts "gem: --commands" + fp.puts "gemhome: #{other_gem_home}" + fp.puts "gempath:" + fp.puts " - #{other_gem_path}" + fp.puts "rdoc: --all" + end + + gr = Gem::GemRunner.new + gr.send :do_configuration, %W[--config-file #{temp_conf}] + + assert_equal [other_gem_path, other_gem_home], Gem.path + assert_equal %w[--commands], Gem::Command.extra_args + assert_equal %w[--all], Gem::DocManager.configured_args + end + +end + diff --git a/test/rubygems/test_gem_indexer.rb b/test/rubygems/test_gem_indexer.rb new file mode 100644 index 0000000000..de509c6b97 --- /dev/null +++ b/test/rubygems/test_gem_indexer.rb @@ -0,0 +1,103 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') + +require 'rubygems/indexer' + +unless ''.respond_to? :to_xs then + warn "Gem::Indexer tests are being skipped. Install builder gem." +end + +class TestGemIndexer < RubyGemTestCase + + def setup + super + + util_make_gems + + gems = File.join(@tempdir, 'gems') + FileUtils.mkdir_p gems + cache_gems = File.join @gemhome, 'cache', '*.gem' + FileUtils.mv Dir[cache_gems], gems + + @indexer = Gem::Indexer.new @tempdir + end + + def test_initialize + assert_equal @tempdir, @indexer.dest_directory + assert_equal File.join(Dir.tmpdir, "gem_generate_index_#{$$}"), + @indexer.directory + end + + def test_generate_index + use_ui @ui do + @indexer.generate_index + end + + assert File.exist?(File.join(@tempdir, 'yaml')) + assert File.exist?(File.join(@tempdir, 'yaml.Z')) + assert File.exist?(File.join(@tempdir, "Marshal.#{@marshal_version}")) + assert File.exist?(File.join(@tempdir, "Marshal.#{@marshal_version}.Z")) + + quickdir = File.join @tempdir, 'quick' + marshal_quickdir = File.join quickdir, "Marshal.#{@marshal_version}" + + assert File.directory?(quickdir) + assert File.directory?(marshal_quickdir) + assert File.exist?(File.join(quickdir, "index")) + assert File.exist?(File.join(quickdir, "index.rz")) + assert File.exist?(File.join(quickdir, "#{@a0_0_1.full_name}.gemspec.rz")) + assert File.exist?(File.join(marshal_quickdir, "#{@a0_0_1.full_name}.gemspec.rz")) + assert File.exist?(File.join(quickdir, "#{@a0_0_2.full_name}.gemspec.rz")) + assert File.exist?(File.join(marshal_quickdir, "#{@a0_0_2.full_name}.gemspec.rz")) + assert File.exist?(File.join(quickdir, "#{@b0_0_2.full_name}.gemspec.rz")) + assert File.exist?(File.join(quickdir, "#{@c1_2.full_name}.gemspec.rz")) + assert !File.exist?(File.join(quickdir, "#{@c1_2.full_name}.gemspec")) + assert !File.exist?(File.join(marshal_quickdir, "#{@c1_2.full_name}.gemspec")) + end + + def test_generate_index_ui + use_ui @ui do + @indexer.generate_index + end + + expected = <<-EOF +Generating index for 4 gems in #{@tempdir} +.... +Loaded all gems +Generating master indexes (this may take a while) + EOF + + assert_equal expected, @ui.output + assert_equal '', @ui.error + end + + def test_generate_index_contents + use_ui @ui do + @indexer.generate_index + end + + yaml_path = File.join(@tempdir, 'yaml') + dump_path = File.join(@tempdir, "Marshal.#{@marshal_version}") + + yaml_index = YAML.load_file(yaml_path) + dump_str = nil + File.open dump_path, 'rb' do |fp| dump_str = fp.read end + dump_index = Marshal.load dump_str + + dump_index.each do |_,gem| + gem.send :remove_instance_variable, :@loaded + gem.send :remove_instance_variable, :@original_platform + end + + assert_equal yaml_index, dump_index, + "expected YAML and Marshal to produce identical results" + end + +end if ''.respond_to? :to_xs + diff --git a/test/rubygems/test_gem_install_update_options.rb b/test/rubygems/test_gem_install_update_options.rb new file mode 100644 index 0000000000..dafdf65920 --- /dev/null +++ b/test/rubygems/test_gem_install_update_options.rb @@ -0,0 +1,40 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/install_update_options' +require 'rubygems/command' + +class TestGemInstallUpdateOptions < RubyGemTestCase + + def setup + super + + @cmd = Gem::Command.new 'dummy', 'dummy' + @cmd.extend Gem::InstallUpdateOptions + end + + def test_add_install_update_options + @cmd.add_install_update_options + + args = %w[-i /install_to --rdoc --ri -E -f -t -w -P HighSecurity + --ignore-dependencies --include-dependencies] + + assert @cmd.handles?(args) + end + + def test_security_policy + @cmd.add_install_update_options + + @cmd.handle_options %w[-P HighSecurity] + + assert_equal Gem::Security::HighSecurity, @cmd.options[:security_policy] + end + + def test_security_policy_unknown + @cmd.add_install_update_options + + assert_raise OptionParser::InvalidArgument do + @cmd.handle_options %w[-P UnknownSecurity] + end + end + +end diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb new file mode 100644 index 0000000000..05e38f67f8 --- /dev/null +++ b/test/rubygems/test_gem_installer.rb @@ -0,0 +1,796 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/installer' + +class Gem::Installer + attr_writer :format + attr_writer :gem_dir + attr_writer :gem_home + attr_writer :env_shebang + attr_writer :ignore_dependencies + attr_writer :security_policy + attr_writer :spec + attr_writer :wrappers +end + +class TestGemInstaller < RubyGemTestCase + + def setup + super + + @spec = quick_gem "a" + @gem = File.join @tempdir, "#{@spec.full_name}.gem" + + util_build_gem @spec + FileUtils.mv File.join(@gemhome, 'cache', "#{@spec.full_name}.gem"), + @tempdir + + @installer = Gem::Installer.new @gem + @installer.gem_dir = util_gem_dir + @installer.gem_home = @gemhome + @installer.spec = @spec + end + + def util_gem_dir(version = '0.0.2') + File.join @gemhome, "gems", "a-#{version}" # HACK + end + + def util_gem_bindir(version = '0.0.2') + File.join util_gem_dir(version), "bin" + end + + def util_inst_bindir + File.join @gemhome, "bin" + end + + def util_make_exec(version = '0.0.2', shebang = "#!/usr/bin/ruby") + @spec.executables = ["my_exec"] + + FileUtils.mkdir_p util_gem_bindir(version) + exec_file = File.join(util_gem_bindir(version), "my_exec") + File.open exec_file, 'w' do |f| + f.puts shebang + end + end + + def test_app_script_text + util_make_exec '0.0.2', '' + + expected = <<-EOF +#!#{Gem.ruby} +# +# This file was generated by RubyGems. +# +# The application 'a' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'rubygems' + +version = \">= 0\" + +if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then + version = $1 + ARGV.shift +end + +gem 'a', version +load 'my_exec' + EOF + + wrapper = @installer.app_script_text 'my_exec' + assert_equal expected, wrapper + end + + def test_build_extensions_none + use_ui @ui do + @installer.build_extensions + end + + assert_equal '', @ui.output + assert_equal '', @ui.error + + assert !File.exist?('gem_make.out') + end + + def test_build_extensions_extconf_bad + @spec.extensions << 'extconf.rb' + + e = assert_raise Gem::Installer::ExtensionBuildError do + use_ui @ui do + @installer.build_extensions + end + end + + assert_match(/\AERROR: Failed to build gem native extension.$/, e.message) + + assert_equal "Building native extensions. This could take a while...\n", + @ui.output + assert_equal '', @ui.error + + gem_make_out = File.join @gemhome, 'gems', @spec.full_name, 'gem_make.out' + expected = <<-EOF +#{Gem.ruby} extconf.rb +#{Gem.ruby}: No such file or directory -- extconf.rb (LoadError) + EOF + + assert_equal expected, File.read(gem_make_out) + end + + def test_build_extensions_unsupported + @spec.extensions << nil + + e = assert_raise Gem::Installer::ExtensionBuildError do + use_ui @ui do + @installer.build_extensions + end + end + + assert_match(/^No builder for extension ''$/, e.message) + + assert_equal "Building native extensions. This could take a while...\n", + @ui.output + assert_equal '', @ui.error + + assert_equal "No builder for extension ''\n", File.read('gem_make.out') + ensure + FileUtils.rm_f 'gem_make.out' + end + + def test_ensure_dependency + dep = Gem::Dependency.new 'a', '>= 0.0.2' + assert @installer.ensure_dependency(@spec, dep) + + dep = Gem::Dependency.new 'b', '> 0.0.2' + e = assert_raise Gem::InstallError do + @installer.ensure_dependency @spec, dep + end + + assert_equal 'a requires b (> 0.0.2)', e.message + end + + def test_expand_and_validate_gem_dir + @installer.gem_dir = '/nonexistent' + expanded_gem_dir = @installer.send(:expand_and_validate_gem_dir) + if win_platform? + expected = File.join(Config::CONFIG['bindir'][0..2], 'nonexistent').downcase + expanded_gem_dir = expanded_gem_dir.downcase + else + expected = '/nonexistent' + end + + assert_equal expected, expanded_gem_dir + end + + def test_extract_files + format = Object.new + def format.file_entries + [[{'size' => 7, 'mode' => 0400, 'path' => 'thefile'}, 'thefile']] + end + + @installer.format = format + + @installer.extract_files + + assert_equal 'thefile', File.read(File.join(util_gem_dir, 'thefile')) + end + + def test_extract_files_bad_dest + @installer.gem_dir = 'somedir' + @installer.format = nil + e = assert_raise ArgumentError do + @installer.extract_files + end + + assert_equal 'format required to extract from', e.message + end + + def test_extract_files_relative + format = Object.new + def format.file_entries + [[{'size' => 10, 'mode' => 0644, 'path' => '../thefile'}, '../thefile']] + end + + @installer.format = format + + e = assert_raise Gem::InstallError do + @installer.extract_files + end + + assert_equal "attempt to install file into \"../thefile\" under #{util_gem_dir.inspect}", + e.message + assert_equal false, File.file?(File.join(@tempdir, '../thefile')), + "You may need to remove this file if you broke the test once" + end + + def test_extract_files_absolute + format = Object.new + def format.file_entries + [[{'size' => 8, 'mode' => 0644, 'path' => '/thefile'}, '/thefile']] + end + + @installer.format = format + + e = assert_raise Gem::InstallError do + @installer.extract_files + end + + assert_equal 'attempt to install file into "/thefile"', e.message + assert_equal false, File.file?(File.join('/thefile')), + "You may need to remove this file if you broke the test once" + end + + def test_generate_bin_scripts + @installer.wrappers = true + util_make_exec + @installer.gem_dir = util_gem_dir + + @installer.generate_bin + assert_equal true, File.directory?(util_inst_bindir) + installed_exec = File.join(util_inst_bindir, "my_exec") + assert_equal true, File.exist?(installed_exec) + assert_equal(0100755, File.stat(installed_exec).mode) unless win_platform? + + wrapper = File.read installed_exec + assert_match %r|generated by RubyGems|, wrapper + end + + def test_generate_bin_scripts_install_dir + @installer.wrappers = true + @spec.executables = ["my_exec"] + + gem_dir = File.join "#{@gemhome}2", 'gems', @spec.full_name + gem_bindir = File.join gem_dir, 'bin' + FileUtils.mkdir_p gem_bindir + File.open File.join(gem_bindir, "my_exec"), 'w' do |f| + f.puts "#!/bin/ruby" + end + + @installer.gem_home = "#{@gemhome}2" + @installer.gem_dir = gem_dir + + @installer.generate_bin + + installed_exec = File.join("#{@gemhome}2", 'bin', 'my_exec') + assert_equal true, File.exist?(installed_exec) + assert_equal(0100755, File.stat(installed_exec).mode) unless win_platform? + + wrapper = File.read installed_exec + assert_match %r|generated by RubyGems|, wrapper + end + + def test_generate_bin_scripts_no_execs + @installer.wrappers = true + @installer.generate_bin + assert_equal false, File.exist?(util_inst_bindir) + end + + def test_generate_bin_scripts_no_perms + @installer.wrappers = true + util_make_exec + + Dir.mkdir util_inst_bindir + File.chmod 0000, util_inst_bindir + + assert_raises Gem::FilePermissionError do + @installer.generate_bin + end + + ensure + File.chmod 0700, util_inst_bindir unless $DEBUG + end + + def test_generate_bin_symlinks + return if win_platform? #Windows FS do not support symlinks + + @installer.wrappers = false + util_make_exec + @installer.gem_dir = util_gem_dir + + @installer.generate_bin + assert_equal true, File.directory?(util_inst_bindir) + installed_exec = File.join(util_inst_bindir, "my_exec") + assert_equal true, File.symlink?(installed_exec) + assert_equal(File.join(util_gem_dir, "bin", "my_exec"), + File.readlink(installed_exec)) + end + + def test_generate_bin_symlinks_no_execs + @installer.wrappers = false + @installer.generate_bin + assert_equal false, File.exist?(util_inst_bindir) + end + + def test_generate_bin_symlinks_no_perms + @installer.wrappers = false + util_make_exec + @installer.gem_dir = util_gem_dir + + Dir.mkdir util_inst_bindir + File.chmod 0000, util_inst_bindir + + assert_raises Gem::FilePermissionError do + @installer.generate_bin + end + + ensure + File.chmod 0700, util_inst_bindir unless $DEBUG + end + + def test_generate_bin_symlinks_update_newer + return if win_platform? #Windows FS do not support symlinks + + @installer.wrappers = false + util_make_exec + @installer.gem_dir = util_gem_dir + + @installer.generate_bin + installed_exec = File.join(util_inst_bindir, "my_exec") + assert_equal(File.join(util_gem_dir, "bin", "my_exec"), + File.readlink(installed_exec)) + + @spec = Gem::Specification.new do |s| + s.files = ['lib/code.rb'] + s.name = "a" + s.version = "0.0.3" + s.summary = "summary" + s.description = "desc" + s.require_path = 'lib' + end + + util_make_exec '0.0.3' + @installer.gem_dir = File.join util_gem_dir('0.0.3') + @installer.generate_bin + installed_exec = File.join(util_inst_bindir, "my_exec") + assert_equal(File.join(util_gem_bindir('0.0.3'), "my_exec"), + File.readlink(installed_exec), + "Ensure symlink moved to latest version") + end + + def test_generate_bin_symlinks_update_older + return if win_platform? #Windows FS do not support symlinks + + @installer.wrappers = false + util_make_exec + @installer.gem_dir = util_gem_dir + + @installer.generate_bin + installed_exec = File.join(util_inst_bindir, "my_exec") + assert_equal(File.join(util_gem_dir, "bin", "my_exec"), + File.readlink(installed_exec)) + + spec = Gem::Specification.new do |s| + s.files = ['lib/code.rb'] + s.name = "a" + s.version = "0.0.1" + s.summary = "summary" + s.description = "desc" + s.require_path = 'lib' + end + + util_make_exec '0.0.1' + @installer.gem_dir = util_gem_dir('0.0.1') + @installer.spec = spec + + @installer.generate_bin + + installed_exec = File.join(util_inst_bindir, "my_exec") + assert_equal(File.join(util_gem_dir('0.0.2'), "bin", "my_exec"), + File.readlink(installed_exec), + "Ensure symlink not moved") + end + + def test_generate_bin_symlinks_update_remove_wrapper + return if win_platform? #Windows FS do not support symlinks + + @installer.wrappers = true + util_make_exec + @installer.gem_dir = util_gem_dir + + @installer.generate_bin + installed_exec = File.join(util_inst_bindir, "my_exec") + assert_equal true, File.exist?(installed_exec) + + @spec = Gem::Specification.new do |s| + s.files = ['lib/code.rb'] + s.name = "a" + s.version = "0.0.3" + s.summary = "summary" + s.description = "desc" + s.require_path = 'lib' + end + + @installer.wrappers = false + util_make_exec '0.0.3' + @installer.gem_dir = util_gem_dir '0.0.3' + @installer.generate_bin + installed_exec = File.join(util_inst_bindir, "my_exec") + assert_equal(File.join(util_gem_dir('0.0.3'), "bin", "my_exec"), + File.readlink(installed_exec), + "Ensure symlink moved to latest version") + end + + def test_generate_bin_symlinks_win32 + old_arch = Config::CONFIG["arch"] + Config::CONFIG["arch"] = "win32" + @installer.wrappers = false + util_make_exec + @installer.gem_dir = util_gem_dir + + use_ui @ui do + @installer.generate_bin + end + + assert_equal true, File.directory?(util_inst_bindir) + installed_exec = File.join(util_inst_bindir, "my_exec") + assert_equal true, File.exist?(installed_exec) + + assert_match(/Unable to use symlinks on win32, installing wrapper/i, + @ui.error) + + expected_mode = win_platform? ? 0100644 : 0100755 + assert_equal expected_mode, File.stat(installed_exec).mode + + wrapper = File.read installed_exec + assert_match(/generated by RubyGems/, wrapper) + ensure + Config::CONFIG["arch"] = old_arch + end + + def test_generate_bin_uses_default_shebang + return if win_platform? #Windows FS do not support symlinks + + @installer.wrappers = true + util_make_exec + + @installer.generate_bin + + default_shebang = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']) + shebang_line = open("#{@gemhome}/bin/my_exec") { |f| f.readlines.first } + assert_match(/\A#!/, shebang_line) + assert_match(/#{default_shebang}/, shebang_line) + end + + def test_install + util_setup_gem + + use_ui @ui do + assert_equal @spec, @installer.install + end + + gemdir = File.join @gemhome, 'gems', @spec.full_name + assert File.exist?(gemdir) + + exe = File.join(gemdir, 'bin', 'executable') + assert File.exist?(exe) + exe_mode = File.stat(exe).mode & 0111 + assert_equal 0111, exe_mode, "0%o" % exe_mode unless win_platform? + + assert File.exist?(File.join(gemdir, 'lib', 'code.rb')) + + assert File.exist?(File.join(gemdir, 'ext', 'a', 'Rakefile')) + + spec_file = File.join(@gemhome, 'specifications', + "#{@spec.full_name}.gemspec") + + assert_equal spec_file, @spec.loaded_from + assert File.exist?(spec_file) + end + + def test_install_bad_gem + gem = nil + + use_ui @ui do + Dir.chdir @tempdir do Gem::Builder.new(@spec).build end + gem = File.join @tempdir, "#{@spec.full_name}.gem" + end + + gem_data = File.open gem, 'rb' do |fp| fp.read 1024 end + File.open gem, 'wb' do |fp| fp.write gem_data end + + e = assert_raise Gem::InstallError do + use_ui @ui do + @installer = Gem::Installer.new gem + @installer.install + end + end + + assert_equal "invalid gem format for #{gem}", e.message + end + + def test_install_check_dependencies + @spec.add_dependency 'b', '> 5' + util_setup_gem + + use_ui @ui do + assert_raise Gem::InstallError do + @installer.install + end + end + end + + def test_install_force + use_ui @ui do + installer = Gem::Installer.new old_ruby_required, :force => true + installer.install + end + + gem_dir = File.join(@gemhome, 'gems', 'old_ruby_required-0.0.1') + assert File.exist?(gem_dir) + end + + def test_install_ignore_dependencies + @spec.add_dependency 'b', '> 5' + util_setup_gem + @installer.ignore_dependencies = true + + use_ui @ui do + assert_equal @spec, @installer.install + end + + gemdir = File.join @gemhome, 'gems', @spec.full_name + assert File.exist?(gemdir) + + exe = File.join(gemdir, 'bin', 'executable') + assert File.exist?(exe) + exe_mode = File.stat(exe).mode & 0111 + assert_equal 0111, exe_mode, "0%o" % exe_mode unless win_platform? + assert File.exist?(File.join(gemdir, 'lib', 'code.rb')) + + assert File.exist?(File.join(@gemhome, 'specifications', + "#{@spec.full_name}.gemspec")) + end + + def test_install_missing_dirs + FileUtils.rm_f File.join(Gem.dir, 'cache') + FileUtils.rm_f File.join(Gem.dir, 'docs') + FileUtils.rm_f File.join(Gem.dir, 'specifications') + + use_ui @ui do + Dir.chdir @tempdir do Gem::Builder.new(@spec).build end + gem = File.join @tempdir, "#{@spec.full_name}.gem" + + @installer.install + end + + File.directory? File.join(Gem.dir, 'cache') + File.directory? File.join(Gem.dir, 'docs') + File.directory? File.join(Gem.dir, 'specifications') + + assert File.exist?(File.join(@gemhome, 'cache', "#{@spec.full_name}.gem")) + assert File.exist?(File.join(@gemhome, 'specifications', + "#{@spec.full_name}.gemspec")) + end + + def test_install_with_message + @spec.post_install_message = 'I am a shiny gem!' + + use_ui @ui do + Dir.chdir @tempdir do Gem::Builder.new(@spec).build end + + @installer.install + end + + assert_match %r|I am a shiny gem!|, @ui.output + end + + def test_install_writable + util_setup_gem + + orig_mode = File.stat(Gem.dir).mode + File.chmod 0000, Gem.dir + + e = assert_raise Gem::FilePermissionError do + @installer.install + end + + assert_equal "You don't have write permissions into the #{@gemhome} directory.", + e.message + ensure + File.chmod orig_mode, Gem.dir + end + + def test_install_wrong_ruby_version + use_ui @ui do + installer = Gem::Installer.new old_ruby_required + e = assert_raise Gem::InstallError do + installer.install + end + assert_equal 'old_ruby_required requires Ruby version = 1.4.6', + e.message + end + end + + def test_install_wrong_rubygems_version + spec = quick_gem 'old_rubygems_required', '0.0.1' do |s| + s.required_rubygems_version = '< 0.0.0' + end + + util_build_gem spec + + gem = File.join @gemhome, 'cache', "#{spec.full_name}.gem" + + use_ui @ui do + @installer = Gem::Installer.new gem + e = assert_raise Gem::InstallError do + @installer.install + end + assert_equal 'old_rubygems_required requires RubyGems version < 0.0.0', + e.message + end + end + + def test_installation_satisfies_dependency_eh + dep = Gem::Dependency.new 'a', '>= 0.0.2' + assert @installer.installation_satisfies_dependency?(dep) + + dep = Gem::Dependency.new 'a', '> 0.0.2' + assert ! @installer.installation_satisfies_dependency?(dep) + end + + def test_shebang + util_make_exec '0.0.2', "#!/usr/bin/ruby" + + shebang = @installer.shebang 'my_exec' + + assert_equal "#!#{Gem.ruby}", shebang + end + + def test_shebang_arguments + util_make_exec '0.0.2', "#!/usr/bin/ruby -ws" + + shebang = @installer.shebang 'my_exec' + + assert_equal "#!#{Gem.ruby} -ws", shebang + end + + def test_shebang_empty + util_make_exec '0.0.2', '' + + shebang = @installer.shebang 'my_exec' + assert_equal "#!#{Gem.ruby}", shebang + end + + def test_shebang_env + util_make_exec '0.0.2', "#!/usr/bin/env ruby" + + shebang = @installer.shebang 'my_exec' + + assert_equal "#!#{Gem.ruby}", shebang + end + + def test_shebang_env_arguments + util_make_exec '0.0.2', "#!/usr/bin/env ruby -ws" + + shebang = @installer.shebang 'my_exec' + + assert_equal "#!#{Gem.ruby} -ws", shebang + end + + def test_shebang_env_shebang + util_make_exec '0.0.2', '' + @installer.env_shebang = true + + shebang = @installer.shebang 'my_exec' + assert_equal "#!/usr/bin/env ruby", shebang + end + + def test_shebang_nested + util_make_exec '0.0.2', "#!/opt/local/ruby/bin/ruby" + + shebang = @installer.shebang 'my_exec' + + assert_equal "#!#{Gem.ruby}", shebang + end + + def test_shebang_nested_arguments + util_make_exec '0.0.2', "#!/opt/local/ruby/bin/ruby -ws" + + shebang = @installer.shebang 'my_exec' + + assert_equal "#!#{Gem.ruby} -ws", shebang + end + + def test_shebang_version + util_make_exec '0.0.2', "#!/usr/bin/ruby18" + + shebang = @installer.shebang 'my_exec' + + assert_equal "#!#{Gem.ruby}", shebang + end + + def test_shebang_version_arguments + util_make_exec '0.0.2', "#!/usr/bin/ruby18 -ws" + + shebang = @installer.shebang 'my_exec' + + assert_equal "#!#{Gem.ruby} -ws", shebang + end + + def test_shebang_version_env + util_make_exec '0.0.2', "#!/usr/bin/env ruby18" + + shebang = @installer.shebang 'my_exec' + + assert_equal "#!#{Gem.ruby}", shebang + end + + def test_shebang_version_env_arguments + util_make_exec '0.0.2', "#!/usr/bin/env ruby18 -ws" + + shebang = @installer.shebang 'my_exec' + + assert_equal "#!#{Gem.ruby} -ws", shebang + end + + def test_unpack + util_setup_gem + + dest = File.join @gemhome, 'gems', @spec.full_name + + @installer.unpack dest + + assert File.exist?(File.join(dest, 'lib', 'code.rb')) + assert File.exist?(File.join(dest, 'bin', 'executable')) + end + + def test_write_spec + spec_dir = File.join @gemhome, 'specifications' + spec_file = File.join spec_dir, "#{@spec.full_name}.gemspec" + FileUtils.rm spec_file + assert !File.exist?(spec_file) + + @installer.spec = @spec + @installer.gem_home = @gemhome + + @installer.write_spec + + assert File.exist?(spec_file) + assert_equal @spec, eval(File.read(spec_file)) + end + + def old_ruby_required + spec = quick_gem 'old_ruby_required', '0.0.1' do |s| + s.required_ruby_version = '= 1.4.6' + end + + util_build_gem spec + + File.join @gemhome, 'cache', "#{spec.full_name}.gem" + end + + def util_setup_gem + @spec.files = File.join('lib', 'code.rb') + @spec.executables << 'executable' + @spec.extensions << File.join('ext', 'a', 'mkrf_conf.rb') + + Dir.chdir @tempdir do + FileUtils.mkdir_p 'bin' + FileUtils.mkdir_p 'lib' + FileUtils.mkdir_p File.join('ext', 'a') + File.open File.join('bin', 'executable'), 'w' do |f| f.puts '1' end + File.open File.join('lib', 'code.rb'), 'w' do |f| f.puts '1' end + File.open File.join('ext', 'a', 'mkrf_conf.rb'), 'w' do |f| + f << <<-EOF + File.open 'Rakefile', 'w' do |rf| rf.puts "task :default" end + EOF + end + + use_ui @ui do + FileUtils.rm @gem + Gem::Builder.new(@spec).build + end + end + + @installer = Gem::Installer.new @gem + end + +end + + diff --git a/test/rubygems/test_gem_local_remote_options.rb b/test/rubygems/test_gem_local_remote_options.rb new file mode 100644 index 0000000000..d5a6651ade --- /dev/null +++ b/test/rubygems/test_gem_local_remote_options.rb @@ -0,0 +1,84 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/local_remote_options' +require 'rubygems/command' + +class TestGemLocalRemoteOptions < RubyGemTestCase + + def setup + super + + @cmd = Gem::Command.new 'dummy', 'dummy' + @cmd.extend Gem::LocalRemoteOptions + end + + def test_add_local_remote_options + @cmd.add_local_remote_options + + args = %w[-l -r -b -B 10 --source http://gems.example.com -p --update-sources] + assert @cmd.handles?(args) + end + + def test_local_eh + assert_equal false, @cmd.local? + + @cmd.options[:domain] = :local + + assert_equal true, @cmd.local? + + @cmd.options[:domain] = :both + + assert_equal true, @cmd.local? + end + + def test_remote_eh + assert_equal false, @cmd.remote? + + @cmd.options[:domain] = :remote + + assert_equal true, @cmd.remote? + + @cmd.options[:domain] = :both + + assert_equal true, @cmd.remote? + end + + def test_source_option + @cmd.add_source_option + + s1 = URI.parse 'http://more-gems.example.com' + s2 = URI.parse 'http://even-more-gems.example.com' + + @cmd.handle_options %W[--source #{s1} --source #{s2}] + + assert_equal [s1, s2], Gem.sources + end + + def test_update_sources_option + @cmd.add_update_sources_option + + Gem.configuration.update_sources = false + + @cmd.handle_options %W[--update-sources] + + assert_equal true, Gem.configuration.update_sources + + @cmd.handle_options %W[--no-update-sources] + + assert_equal false, Gem.configuration.update_sources + end + + def test_source_option_bad + @cmd.add_source_option + + s1 = 'htp://more-gems.example.com' + + assert_raise OptionParser::InvalidArgument do + @cmd.handle_options %W[--source #{s1}] + end + + assert_equal %w[http://gems.example.com], Gem.sources + end + +end + diff --git a/test/rubygems/test_gem_outdated_command.rb b/test/rubygems/test_gem_outdated_command.rb new file mode 100644 index 0000000000..adcc4d1980 --- /dev/null +++ b/test/rubygems/test_gem_outdated_command.rb @@ -0,0 +1,40 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/commands/outdated_command' + +class TestGemOutdatedCommand < RubyGemTestCase + + def setup + super + + @cmd = Gem::Commands::OutdatedCommand.new + end + + def test_initialize + assert @cmd.handles?(%W[--platform #{Gem::Platform.local}]) + end + + def test_execute + local_01 = quick_gem 'foo', '0.1' + local_02 = quick_gem 'foo', '0.2' + remote_10 = quick_gem 'foo', '1.0' + remote_20 = quick_gem 'foo', '2.0' + + remote_spec_file = File.join @gemhome, 'specifications', + remote_10.full_name + ".gemspec" + FileUtils.rm remote_spec_file + + remote_spec_file = File.join @gemhome, 'specifications', + remote_20.full_name + ".gemspec" + FileUtils.rm remote_spec_file + + util_setup_source_info_cache remote_10, remote_20 + + use_ui @ui do @cmd.execute end + + assert_equal "foo (0.2 < 2.0)\n", @ui.output + assert_equal "", @ui.error + end + +end + diff --git a/test/rubygems/test_gem_platform.rb b/test/rubygems/test_gem_platform.rb new file mode 100644 index 0000000000..4c583edf27 --- /dev/null +++ b/test/rubygems/test_gem_platform.rb @@ -0,0 +1,239 @@ +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'test/unit' +require 'rubygems/platform' +require 'rbconfig' + +class TestGemPlatform < RubyGemTestCase + + def test_self_local + util_set_arch 'i686-darwin8.10.1' + + assert_equal Gem::Platform.new(%w[x86 darwin 8]), Gem::Platform.local + end + + def test_self_match + assert Gem::Platform.match(nil), 'nil == ruby' + assert Gem::Platform.match(Gem::Platform.local), 'exact match' + assert Gem::Platform.match(Gem::Platform.local.to_s), '=~ match' + assert Gem::Platform.match(Gem::Platform::RUBY), 'ruby' + end + + def test_self_new + assert_equal Gem::Platform::RUBY, Gem::Platform.new(Gem::Platform::RUBY) + assert_equal Gem::Platform::RUBY, Gem::Platform.new(nil) + end + + def test_initialize + test_cases = { + 'amd64-freebsd6' => ['amd64', 'freebsd', '6'], + 'hppa2.0w-hpux11.31' => ['hppa2.0w', 'hpux', '11'], + 'java' => [nil, 'java', nil], + 'jruby' => [nil, 'java', nil], + 'powerpc-aix5.3.0.0' => ['powerpc', 'aix', '5'], + 'powerpc-darwin7' => ['powerpc', 'darwin', '7'], + 'powerpc-darwin8' => ['powerpc', 'darwin', '8'], + 'powerpc-linux' => ['powerpc', 'linux', nil], + 'powerpc64-linux' => ['powerpc64', 'linux', nil], + 'sparc-solaris2.10' => ['sparc', 'solaris', '2.10'], + 'sparc-solaris2.8' => ['sparc', 'solaris', '2.8'], + 'sparc-solaris2.9' => ['sparc', 'solaris', '2.9'], + 'universal-darwin8' => ['universal', 'darwin', '8'], + 'universal-darwin9' => ['universal', 'darwin', '9'], + 'i386-cygwin' => ['x86', 'cygwin', nil], + 'i686-darwin' => ['x86', 'darwin', nil], + 'i686-darwin8.4.1' => ['x86', 'darwin', '8'], + 'i386-freebsd4.11' => ['x86', 'freebsd', '4'], + 'i386-freebsd5' => ['x86', 'freebsd', '5'], + 'i386-freebsd6' => ['x86', 'freebsd', '6'], + 'i386-freebsd7' => ['x86', 'freebsd', '7'], + 'i386-java1.5' => ['x86', 'java', '1.5'], + 'x86-java1.6' => ['x86', 'java', '1.6'], + 'i386-java1.6' => ['x86', 'java', '1.6'], + 'i686-linux' => ['x86', 'linux', nil], + 'i586-linux' => ['x86', 'linux', nil], + 'i486-linux' => ['x86', 'linux', nil], + 'i386-linux' => ['x86', 'linux', nil], + 'i586-linux-gnu' => ['x86', 'linux', nil], + 'i386-linux-gnu' => ['x86', 'linux', nil], + 'i386-mingw32' => ['x86', 'mingw32', nil], + 'i386-mswin32' => ['x86', 'mswin32', nil], + 'i386-mswin32_80' => ['x86', 'mswin32', '80'], + 'i386-netbsdelf' => ['x86', 'netbsdelf', nil], + 'i386-openbsd4.0' => ['x86', 'openbsd', '4.0'], + 'i386-solaris2.10' => ['x86', 'solaris', '2.10'], + 'i386-solaris2.8' => ['x86', 'solaris', '2.8'], + 'x86_64-linux' => ['x86_64', 'linux', nil], + 'x86_64-openbsd3.9' => ['x86_64', 'openbsd', '3.9'], + 'x86_64-openbsd4.0' => ['x86_64', 'openbsd', '4.0'], + } + + test_cases.each do |arch, expected| + platform = Gem::Platform.new arch + assert_equal expected, platform.to_a, arch.inspect + end + end + + def test_initialize_command_line + expected = ['x86', 'mswin32', nil] + + platform = Gem::Platform.new 'i386-mswin32' + + assert_equal expected, platform.to_a, 'i386-mswin32' + + expected = ['x86', 'mswin32', '80'] + + platform = Gem::Platform.new 'i386-mswin32-80' + + assert_equal expected, platform.to_a, 'i386-mswin32-80' + end + + def test_initialize_mswin32_vc6 + orig_RUBY_SO_NAME = Config::CONFIG['RUBY_SO_NAME'] + Config::CONFIG['RUBY_SO_NAME'] = 'msvcrt-ruby18' + + expected = ['x86', 'mswin32', nil] + + platform = Gem::Platform.new 'i386-mswin32' + + assert_equal expected, platform.to_a, 'i386-mswin32 VC6' + ensure + Config::CONFIG['RUBY_SO_NAME'] = orig_RUBY_SO_NAME + end + + def test_initialize_platform + platform = Gem::Platform.new 'cpu-my_platform1' + expected = Gem::Platform.new platform + + assert_equal 'cpu', platform.cpu + assert_equal 'my_platform', platform.os + assert_equal '1', platform.version + end + + def test_initialize_test + platform = Gem::Platform.new 'cpu-my_platform1' + assert_equal 'cpu', platform.cpu + assert_equal 'my_platform', platform.os + assert_equal '1', platform.version + + platform = Gem::Platform.new 'cpu-other_platform1' + assert_equal 'cpu', platform.cpu + assert_equal 'other_platform', platform.os + assert_equal '1', platform.version + end + + def test_to_s + if win_platform? then + assert_equal 'x86-mswin32-60', Gem::Platform.local.to_s + else + assert_equal 'x86-darwin-8', Gem::Platform.local.to_s + end + end + + def test_equals2 + my = Gem::Platform.new %w[cpu my_platform 1] + other = Gem::Platform.new %w[cpu other_platform 1] + + assert_equal my, my + assert_not_equal my, other + assert_not_equal other, my + end + + def test_equals3 + my = Gem::Platform.new %w[cpu my_platform 1] + other = Gem::Platform.new %w[cpu other_platform 1] + + assert(my === my) + assert !(other === my) + assert !(my === other) + end + + def test_equals3_cpu + ppc_darwin8 = Gem::Platform.new 'powerpc-darwin8.0' + uni_darwin8 = Gem::Platform.new 'universal-darwin8.0' + x86_darwin8 = Gem::Platform.new 'i686-darwin8.0' + + util_set_arch 'powerpc-darwin8' + assert((ppc_darwin8 === Gem::Platform.local), 'powerpc =~ universal') + assert((uni_darwin8 === Gem::Platform.local), 'powerpc =~ universal') + assert !(x86_darwin8 === Gem::Platform.local), 'powerpc =~ universal' + + util_set_arch 'i686-darwin8' + assert !(ppc_darwin8 === Gem::Platform.local), 'powerpc =~ universal' + assert((uni_darwin8 === Gem::Platform.local), 'x86 =~ universal') + assert((x86_darwin8 === Gem::Platform.local), 'powerpc =~ universal') + + util_set_arch 'universal-darwin8' + assert((ppc_darwin8 === Gem::Platform.local), 'universal =~ ppc') + assert((uni_darwin8 === Gem::Platform.local), 'universal =~ universal') + assert((x86_darwin8 === Gem::Platform.local), 'universal =~ x86') + end + + def test_equals3_version + util_set_arch 'i686-darwin8' + + x86_darwin = Gem::Platform.new ['x86', 'darwin', nil] + x86_darwin7 = Gem::Platform.new ['x86', 'darwin', '7'] + x86_darwin8 = Gem::Platform.new ['x86', 'darwin', '8'] + x86_darwin9 = Gem::Platform.new ['x86', 'darwin', '9'] + + assert((x86_darwin === Gem::Platform.local), 'x86_darwin === x86_darwin8') + assert((x86_darwin8 === Gem::Platform.local), 'x86_darwin8 === x86_darwin8') + + assert !(x86_darwin7 === Gem::Platform.local), 'x86_darwin7 === x86_darwin8' + assert !(x86_darwin9 === Gem::Platform.local), 'x86_darwin9 === x86_darwin8' + end + + def test_equals_tilde + util_set_arch 'i386-mswin32' + + assert_match 'mswin32', Gem::Platform.local + assert_match 'i386-mswin32', Gem::Platform.local + + # oddballs + assert_match 'i386-mswin32-mq5.3', Gem::Platform.local + assert_match 'i386-mswin32-mq6', Gem::Platform.local + deny_match 'win32-1.8.2-VC7', Gem::Platform.local + deny_match 'win32-1.8.4-VC6', Gem::Platform.local + deny_match 'win32-source', Gem::Platform.local + deny_match 'windows', Gem::Platform.local + + util_set_arch 'i686-linux' + assert_match 'i486-linux', Gem::Platform.local + assert_match 'i586-linux', Gem::Platform.local + assert_match 'i686-linux', Gem::Platform.local + + util_set_arch 'i686-darwin8' + assert_match 'i686-darwin8.4.1', Gem::Platform.local + assert_match 'i686-darwin8.8.2', Gem::Platform.local + + util_set_arch 'java' + assert_match 'java', Gem::Platform.local + assert_match 'jruby', Gem::Platform.local + + util_set_arch 'powerpc-darwin' + assert_match 'powerpc-darwin', Gem::Platform.local + + util_set_arch 'powerpc-darwin7' + assert_match 'powerpc-darwin7.9.0', Gem::Platform.local + + util_set_arch 'powerpc-darwin8' + assert_match 'powerpc-darwin8.10.0', Gem::Platform.local + + util_set_arch 'sparc-solaris2.8' + assert_match 'sparc-solaris2.8-mq5.3', Gem::Platform.local + end + + def assert_match(pattern, platform, message = '') + full_message = build_message message, " expected to be =~\n.", + platform, pattern + assert_block(full_message) { platform =~ pattern } + end + + def deny_match(pattern, platform, message = '') + full_message = build_message message, " expected to be !~\n.", + platform, pattern + assert_block(full_message) { platform !~ pattern } + end + +end + diff --git a/test/rubygems/test_gem_remote_fetcher.rb b/test/rubygems/test_gem_remote_fetcher.rb new file mode 100644 index 0000000000..83865e8033 --- /dev/null +++ b/test/rubygems/test_gem_remote_fetcher.rb @@ -0,0 +1,417 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'webrick' +require 'zlib' +require 'rubygems/remote_fetcher' + +# = Testing Proxy Settings +# +# These tests check the proper proxy server settings by running two +# web servers. The web server at http://localhost:#{SERVER_PORT} +# represents the normal gem server and returns a gemspec with a rake +# version of 0.4.11. The web server at http://localhost:#{PROXY_PORT} +# represents the proxy server and returns a different dataset where +# rake has version 0.4.2. This allows us to detect which server is +# returning the data. +# +# Note that the proxy server is not a *real* proxy server. But our +# software doesn't really care, as long as we hit the proxy URL when a +# proxy is configured. +# +class TestGemRemoteFetcher < RubyGemTestCase + + include Gem::DefaultUserInteraction + + SERVER_DATA = <<-EOY +--- !ruby/object:Gem::Cache +gems: + rake-0.4.11: !ruby/object:Gem::Specification + rubygems_version: "0.7" + specification_version: 1 + name: rake + version: !ruby/object:Gem::Version + version: 0.4.11 + date: 2004-11-12 + summary: Ruby based make-like utility. + require_paths: + - lib + author: Jim Weirich + email: jim@weirichhouse.org + homepage: http://rake.rubyforge.org + rubyforge_project: rake + description: Rake is a Make-like program implemented in Ruby. Tasks and dependencies are specified in standard Ruby syntax. + autorequire: + default_executable: rake + bindir: bin + has_rdoc: true + required_ruby_version: !ruby/object:Gem::Version::Requirement + requirements: + - + - ">" + - !ruby/object:Gem::Version + version: 0.0.0 + version: + platform: ruby + files: + - README + test_files: [] + library_stubs: + rdoc_options: + extra_rdoc_files: + executables: + - rake + extensions: [] + requirements: [] + dependencies: [] + EOY + + PROXY_DATA = SERVER_DATA.gsub(/0.4.11/, '0.4.2') + + # don't let 1.8 and 1.9 autotest collide + RUBY_VERSION =~ /(\d+)\.(\d+)\.(\d+)/ + PROXY_PORT = 12345 + $1.to_i * 100 + $2.to_i * 10 + $3.to_i + SERVER_PORT = 23456 + $1.to_i * 100 + $2.to_i * 10 + $3.to_i + + def setup + super + self.class.start_servers + self.class.enable_yaml = true + self.class.enable_zip = false + ENV.delete 'http_proxy' + ENV.delete 'HTTP_PROXY' + ENV.delete 'http_proxy_user' + ENV.delete 'HTTP_PROXY_USER' + ENV.delete 'http_proxy_pass' + ENV.delete 'HTTP_PROXY_PASS' + + base_server_uri = "http://localhost:#{SERVER_PORT}" + @proxy_uri = "http://localhost:#{PROXY_PORT}" + + @server_uri = base_server_uri + "/yaml" + @server_z_uri = base_server_uri + "/yaml.Z" + + Gem::RemoteFetcher.instance_variable_set :@fetcher, nil + end + + def test_self_fetcher + fetcher = Gem::RemoteFetcher.fetcher + assert_not_nil fetcher + assert_kind_of Gem::RemoteFetcher, fetcher + end + + def test_self_fetcher_with_proxy + proxy_uri = 'http://proxy.example.com' + Gem.configuration[:http_proxy] = proxy_uri + fetcher = Gem::RemoteFetcher.fetcher + assert_not_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 + fetcher = Gem::RemoteFetcher.fetcher + assert_not_nil fetcher + assert_kind_of Gem::RemoteFetcher, fetcher + assert_equal proxy_uri, fetcher.instance_variable_get(:@proxy_uri) + end + + def test_fetch_size_bad_uri + fetcher = Gem::RemoteFetcher.new nil + + e = assert_raise ArgumentError do + fetcher.fetch_size 'gems.example.com/yaml' + end + + assert_equal 'uri is not an HTTP URI', e.message + end + + def test_fetch_size_socket_error + fetcher = Gem::RemoteFetcher.new nil + def fetcher.connect_to(host, port) + raise SocketError + end + + e = assert_raise Gem::RemoteFetcher::FetchError do + fetcher.fetch_size 'http://gems.example.com/yaml' + end + + assert_equal 'SocketError (SocketError)', e.message + end + + def test_no_proxy + use_ui @ui do + fetcher = Gem::RemoteFetcher.new nil + assert_data_from_server fetcher.fetch_path(@server_uri) + assert_equal SERVER_DATA.size, fetcher.fetch_size(@server_uri) + end + 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', URI.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', URI.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', URI.unescape(proxy.user) + assert_equal 'my bar', URI.unescape(proxy.password) + assert_data_from_proxy fetcher.fetch_path(@server_uri) + end + end + + def test_fetch_path_io_error + fetcher = Gem::RemoteFetcher.new nil + + def fetcher.open_uri_or_path(uri) raise EOFError; end + + e = assert_raise Gem::RemoteFetcher::FetchError do + fetcher.fetch_path 'uri' + end + + assert_equal 'EOFError: EOFError reading uri', e.message + end + + def test_fetch_path_socket_error + fetcher = Gem::RemoteFetcher.new nil + + def fetcher.open_uri_or_path(uri) raise SocketError; end + + e = assert_raise Gem::RemoteFetcher::FetchError do + fetcher.fetch_path 'uri' + end + + assert_equal 'SocketError: SocketError reading uri', e.message + end + + def test_fetch_path_system_call_error + fetcher = Gem::RemoteFetcher.new nil + + def fetcher.open_uri_or_path(uri); + raise Errno::ECONNREFUSED, 'connect(2)' + end + + e = assert_raise Gem::RemoteFetcher::FetchError do + fetcher.fetch_path 'uri' + end + + assert_match %r|\AErrno::ECONNREFUSED: .* - connect\(2\) reading uri\z|, + e.message + end + + def test_get_proxy_from_env_empty + orig_env_HTTP_PROXY = ENV['HTTP_PROXY'] + orig_env_http_proxy = ENV['http_proxy'] + + ENV['HTTP_PROXY'] = '' + ENV.delete 'http_proxy' + + fetcher = Gem::RemoteFetcher.new nil + + assert_equal nil, fetcher.send(:get_proxy_from_env) + + ensure + orig_env_HTTP_PROXY.nil? ? ENV.delete('HTTP_PROXY') : + ENV['HTTP_PROXY'] = orig_env_HTTP_PROXY + orig_env_http_proxy.nil? ? ENV.delete('http_proxy') : + ENV['http_proxy'] = orig_env_http_proxy + end + + def test_implicit_no_proxy + use_ui @ui do + ENV['http_proxy'] = 'http://fakeurl:12345' + fetcher = Gem::RemoteFetcher.new :no_proxy + assert_data_from_server fetcher.fetch_path(@server_uri) + end + end + + def test_implicit_proxy + use_ui @ui do + ENV['http_proxy'] = @proxy_uri + fetcher = Gem::RemoteFetcher.new nil + assert_data_from_proxy fetcher.fetch_path(@server_uri) + end + end + + def test_implicit_upper_case_proxy + use_ui @ui do + ENV['HTTP_PROXY'] = @proxy_uri + fetcher = Gem::RemoteFetcher.new nil + assert_data_from_proxy fetcher.fetch_path(@server_uri) + end + end + + def test_implicit_proxy_no_env + use_ui @ui do + fetcher = Gem::RemoteFetcher.new nil + assert_data_from_server fetcher.fetch_path(@server_uri) + end + end + + def test_zip + use_ui @ui do + self.class.enable_zip = true + fetcher = Gem::RemoteFetcher.new nil + assert_equal SERVER_DATA.size, fetcher.fetch_size(@server_uri), "probably not from proxy" + zip_data = fetcher.fetch_path(@server_z_uri) + assert zip_data.size < SERVER_DATA.size, "Zipped data should be smaller" + end + end + + def test_no_zip + use_ui @ui do + self.class.enable_zip = false + fetcher = Gem::RemoteFetcher.new nil + assert_error { fetcher.fetch_path(@server_z_uri) } + end + end + + def test_yaml_error_on_size + use_ui @ui do + self.class.enable_yaml = false + fetcher = Gem::RemoteFetcher.new nil + assert_error { fetcher.size } + end + end + + private + + def assert_error(exception_class=Exception) + got_exception = false + begin + yield + rescue exception_class => ex + got_exception = true + end + assert got_exception, "Expected exception conforming to #{exception_class}" + end + + def assert_data_from_server(data) + assert_block("Data is not from server") { data =~ /0\.4\.11/ } + end + + def assert_data_from_proxy(data) + assert_block("Data is not from proxy") { data =~ /0\.4\.2/ } + end + + class NilLog < WEBrick::Log + def log(level, data) #Do nothing + end + end + + class << self + attr_reader :normal_server, :proxy_server + attr_accessor :enable_zip, :enable_yaml + + def start_servers + @normal_server ||= start_server(SERVER_PORT, SERVER_DATA) + @proxy_server ||= start_server(PROXY_PORT, PROXY_DATA) + @enable_yaml = true + @enable_zip = false + end + + private + + def start_server(port, data) + Thread.new do + begin + null_logger = NilLog.new + s = WEBrick::HTTPServer.new( + :Port => port, + :DocumentRoot => nil, + :Logger => null_logger, + :AccessLog => null_logger + ) + s.mount_proc("/kill") { |req, res| s.shutdown } + s.mount_proc("/yaml") { |req, res| + if @enable_yaml + res.body = data + res['Content-Type'] = 'text/plain' + res['content-length'] = data.size + else + res.status = "404" + res.body = "

NOT FOUND

" + res['Content-Type'] = 'text/html' + end + } + s.mount_proc("/yaml.Z") { |req, res| + if @enable_zip + res.body = Zlib::Deflate.deflate(data) + res['Content-Type'] = 'text/plain' + else + res.status = "404" + res.body = "

NOT FOUND

" + res['Content-Type'] = 'text/html' + end + } + s.start + rescue Exception => ex + abort ex.message + puts "ERROR during server thread: #{ex.message}" + end + end + sleep 0.2 # Give the servers time to startup + end + end + +end + diff --git a/test/rubygems/test_gem_remote_installer.rb b/test/rubygems/test_gem_remote_installer.rb new file mode 100644 index 0000000000..90d94843bd --- /dev/null +++ b/test/rubygems/test_gem_remote_installer.rb @@ -0,0 +1,161 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/remote_installer' + +class MockFetcher + def initialize(uri, proxy) + @uri = uri + @proxy = proxy + end + + def size + 1000 + end + + def source_index + if @uri =~ /non.existent.url/ + fail Gem::RemoteSourceException, + "Error fetching remote gem cache: Mock Socket Exception" + end + result = { + 'foo-1.2.3' => Gem::Specification.new do |s| + s.name = 'foo' + s.version = "1.2.3" + s.summary = "This is a cool package" + end, + 'foo-tools-2.0.0' => Gem::Specification.new do |s| + s.name = 'foo-tools' + s.version = "2.0.0" + s.summary = "This is an even cooler package" + end, + 'foo-2-2.0.0' => Gem::Specification.new do |s| + s.name = 'foo-2' + s.version = "2.0.0" + s.summary = "This is the coolest package evar!~!" + end, + } + result + end + + def fetch_path(path) + end + + def self.finish + end +end + +class TestGemRemoteInstaller < RubyGemTestCase + + def setup + super + + util_setup_fake_fetcher + + util_setup_source_info_cache @gem1, @gem4 + + @installer = Gem::RemoteInstaller.new + @installer.instance_variable_set("@fetcher_class", MockFetcher) + end + + def teardown + FileUtils.rm "dest_file" rescue nil + end + + def test_find_gem_to_install + future_gem = quick_gem @gem1.name, '9.9.9' do |spec| + spec.required_ruby_version = '> 999.999.999' # HACK + end + + util_setup_source_info_cache @gem1, future_gem + version = Gem::Version::Requirement.new "> 0.0.0" + gems = @installer.find_gem_to_install(@gem1.name, version) + + assert_equal @gem1.full_name, gems.first.full_name + end + + def test_source_index_hash + source_hash = @installer.source_index_hash + + assert_equal 1, source_hash.size + assert source_hash.has_key?('http://gems.example.com') + assert_equal [@gem1, @gem4], + source_hash['http://gems.example.com'].search(@gem1.name) + end + + def test_specs_n_sources_matching + version = Gem::Version::Requirement.new "> 0.0.0" + specs_n_sources = @installer.specs_n_sources_matching @gem1.name, version + + gems = specs_n_sources.map { |g,| g.full_name } + + assert_equal [@gem1.full_name], gems, + "Gems with longer names and higher versions must not match" + end + +end + +# This test suite has a number of TODOs in the test cases. The +# TestRemoteInstaller test suite is a reworking of this class from +# scratch. +class RemoteInstallerTest #< RubyGemTestCase # HACK disabled + class RInst < Gem::RemoteInstaller + include Test::Unit::Assertions + + attr_accessor :expected_destination_files + attr_accessor :expected_bodies + attr_accessor :caches + attr_accessor :responses + + def source_index_hash + @caches + end + + def fetch(uri) + @reponses ||= {} + @responses[uri] + end + + def write_gem_to_file(body, destination_file) + expected_destination_file = expected_destination_files.pop + expected_body = expected_bodies.pop + assert_equal expected_body, body, "Unexpected body" + assert_equal expected_destination_file, destination_file, "Unexpected destination file" + end + + def new_installer(gem) + return MockInstaller.new(gem) + end + end + + def setup + Gem.clear_paths + @remote_installer = Gem::RemoteInstaller.new + @remote_installer.instance_eval { @fetcher_class = MockFetcher } + end + + SAMPLE_SPEC = Gem::Specification.new do |s| + s.name = 'foo' + s.version = "1.2.3" + s.platform = Gem::Platform::RUBY + s.summary = "This is a cool package" + s.files = [] + end + SAMPLE_CACHE = { 'foo-1.2.3' => SAMPLE_SPEC } + SAMPLE_CACHE_YAML = SAMPLE_CACHE.to_yaml + + FOO_GEM = '' # TODO + CACHE_DIR = File.join(Gem.dir, 'cache') + + def test_install + result = @remote_installer.install('foo') + assert_equal [nil], result + end + +end + diff --git a/test/rubygems/test_gem_requirement.rb b/test/rubygems/test_gem_requirement.rb new file mode 100644 index 0000000000..c441bd16df --- /dev/null +++ b/test/rubygems/test_gem_requirement.rb @@ -0,0 +1,223 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/version' + +class TestGemRequirement < RubyGemTestCase + + def setup + super + + @r1_2 = Gem::Requirement.new '= 1.2' + @r1_3 = Gem::Requirement.new '= 1.3' + end + + def test_initialize + r = Gem::Requirement.new '2' + assert_equal '= 2', r.to_s, 'String' + + r = Gem::Requirement.new %w[2] + assert_equal '= 2', r.to_s, 'Array of Strings' + + r = Gem::Requirement.new Gem::Version.new('2') + assert_equal '= 2', r.to_s, 'Gem::Version' + end + + def test_equals2 + assert_equal @r1_2, @r1_2.dup + assert_equal @r1_2.dup, @r1_2 + + assert_not_equal @r1_3, @r1_2 + assert_not_equal @r1_2, @r1_3 + + assert_not_equal Object.new, @r1_2 + assert_not_equal @r1_2, Object.new + end + + def test_hash + assert_equal @r1_2.hash, @r1_2.dup.hash + assert_equal @r1_2.dup.hash, @r1_2.hash + + assert_not_equal @r1_2.hash, @r1_3.hash + assert_not_equal @r1_3.hash, @r1_2.hash + end + + # We may get some old gems that have requirements in old formats. + # We need to be able to handle those old requirements by normalizing + # them to the latest format. + def test_normalization + require 'yaml' + yamldep = %{--- !ruby/object:Gem::Requirement + nums: + - 1 + - 0 + - 4 + op: ">=" + version: ">= 1.0.4"} + dep = YAML.load(yamldep) + dep.normalize + assert_equal ">= 1.0.4", dep.to_s + end + + def test_parse + assert_equal ['=', Gem::Version.new(1)], @r1_2.parse(' 1') + + assert_equal ['=', Gem::Version.new(1)], @r1_2.parse('= 1') + assert_equal ['>', Gem::Version.new(1)], @r1_2.parse('> 1') + + assert_equal ['=', Gem::Version.new(0)], @r1_2.parse('=') + assert_equal ['>', Gem::Version.new(0)], @r1_2.parse('>') + + assert_equal ['=', Gem::Version.new(1)], @r1_2.parse("=\n1") + assert_equal ['=', Gem::Version.new(0)], @r1_2.parse("=\njunk") + + assert_equal ['=', Gem::Version.new(2)], @r1_2.parse(Gem::Version.new('2')) + end + + def test_parse_illformed + e = assert_raise ArgumentError do + @r1_2.parse(nil) + end + + assert_equal 'Illformed requirement [nil]', e.message + + e = assert_raise ArgumentError do + @r1_2.parse('') + end + + assert_equal 'Illformed requirement [""]', e.message + end + + def test_satisfied_by_eh_bang_equal + r1_2 = Gem::Requirement.new '!= 1.2' + v1_1 = Gem::Version.new '1.1' + v1_2 = Gem::Version.new '1.2' + v1_3 = Gem::Version.new '1.3' + + assert_equal true, r1_2.satisfied_by?(nil) + assert_equal true, r1_2.satisfied_by?(v1_1) + assert_equal false, r1_2.satisfied_by?(v1_2) + assert_equal true, r1_2.satisfied_by?(v1_3) + end + + def test_satisfied_by_eh_blank + r1_2 = Gem::Requirement.new '1.2' + v1_1 = Gem::Version.new '1.1' + v1_2 = Gem::Version.new '1.2' + v1_3 = Gem::Version.new '1.3' + + assert_equal false, r1_2.satisfied_by?(nil) + assert_equal false, r1_2.satisfied_by?(v1_1) + assert_equal true, r1_2.satisfied_by?(v1_2) + assert_equal false, r1_2.satisfied_by?(v1_3) + end + + def test_satisfied_by_eh_equal + r1_2 = @r1_2 + v1_1 = Gem::Version.new '1.1' + v1_2 = Gem::Version.new '1.2' + v1_3 = Gem::Version.new '1.3' + + assert_equal false, r1_2.satisfied_by?(nil) + assert_equal false, r1_2.satisfied_by?(v1_1) + assert_equal true, r1_2.satisfied_by?(v1_2) + assert_equal false, r1_2.satisfied_by?(v1_3) + end + + def test_satisfied_by_eh_gt + r1_2 = Gem::Requirement.new '> 1.2' + v1_1 = Gem::Version.new '1.1' + v1_2 = Gem::Version.new '1.2' + v1_3 = Gem::Version.new '1.3' + + assert_equal false, r1_2.satisfied_by?(v1_1) + assert_equal false, r1_2.satisfied_by?(v1_2) + assert_equal true, r1_2.satisfied_by?(v1_3) + + assert_raise NoMethodError do + assert_equal true, r1_2.satisfied_by?(nil) + end + end + + def test_satisfied_by_eh_gte + r1_2 = Gem::Requirement.new '>= 1.2' + v1_1 = Gem::Version.new '1.1' + v1_2 = Gem::Version.new '1.2' + v1_3 = Gem::Version.new '1.3' + + assert_equal false, r1_2.satisfied_by?(v1_1) + assert_equal true, r1_2.satisfied_by?(v1_2) + assert_equal true, r1_2.satisfied_by?(v1_3) + + assert_raise NoMethodError do + assert_equal true, r1_2.satisfied_by?(nil) + end + end + + def test_satisfied_by_eh_list + r = Gem::Requirement.create(['> 1.1', '< 1.3']) + v1_1 = Gem::Version.new '1.1' + v1_2 = Gem::Version.new '1.2' + v1_3 = Gem::Version.new '1.3' + + assert_equal false, r.satisfied_by?(v1_1) + assert_equal true, r.satisfied_by?(v1_2) + assert_equal false, r.satisfied_by?(v1_3) + + assert_raise NoMethodError do + assert_equal true, r.satisfied_by?(nil) + end + end + + def test_satisfied_by_eh_lt + r1_2 = Gem::Requirement.new '< 1.2' + v1_1 = Gem::Version.new '1.1' + v1_2 = Gem::Version.new '1.2' + v1_3 = Gem::Version.new '1.3' + + assert_equal true, r1_2.satisfied_by?(v1_1) + assert_equal false, r1_2.satisfied_by?(v1_2) + assert_equal false, r1_2.satisfied_by?(v1_3) + + assert_raise NoMethodError do + assert_equal true, r1_2.satisfied_by?(nil) + end + end + + def test_satisfied_by_eh_lte + r1_2 = Gem::Requirement.new '<= 1.2' + v1_1 = Gem::Version.new '1.1' + v1_2 = Gem::Version.new '1.2' + v1_3 = Gem::Version.new '1.3' + + assert_equal true, r1_2.satisfied_by?(v1_1) + assert_equal true, r1_2.satisfied_by?(v1_2) + assert_equal false, r1_2.satisfied_by?(v1_3) + + assert_raise NoMethodError do + assert_equal true, r1_2.satisfied_by?(nil) + end + end + + def test_satisfied_by_eh_tilde_gt + r1_2 = Gem::Requirement.new '~> 1.2' + v1_1 = Gem::Version.new '1.1' + v1_2 = Gem::Version.new '1.2' + v1_3 = Gem::Version.new '1.3' + + assert_equal false, r1_2.satisfied_by?(v1_1) + assert_equal true, r1_2.satisfied_by?(v1_2) + assert_equal true, r1_2.satisfied_by?(v1_3) + + assert_raise NoMethodError do + assert_equal true, r1_2.satisfied_by?(nil) + end + end + +end + diff --git a/test/rubygems/test_gem_server.rb b/test/rubygems/test_gem_server.rb new file mode 100644 index 0000000000..98c8ba5543 --- /dev/null +++ b/test/rubygems/test_gem_server.rb @@ -0,0 +1,71 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/server' +require 'stringio' + +class Gem::Server + attr_reader :server +end + +class TestGemServer < RubyGemTestCase + + def setup + super + + @a1 = quick_gem 'a', '1' + + @server = Gem::Server.new Gem.dir, 8809, false + @req = WEBrick::HTTPRequest.new :Logger => nil + @res = WEBrick::HTTPResponse.new :HTTPVersion => '1.0' + end + + def test_quick_index + data = StringIO.new "GET /quick/index HTTP/1.0\r\n\r\n" + @req.parse data + + @server.quick @req, @res + + assert_match %r| \d\d:\d\d:\d\d |, @res['date'] + assert_equal 'text/plain', @res['content-type'] + assert_equal "a-1", @res.body + end + + def test_quick_index_rz + data = StringIO.new "GET /quick/index.rz HTTP/1.0\r\n\r\n" + @req.parse data + + @server.quick @req, @res + + assert_match %r| \d\d:\d\d:\d\d |, @res['date'] + assert_equal 'text/plain', @res['content-type'] + assert_equal "a-1", Zlib::Inflate.inflate(@res.body) + end + + def test_quick_a_1_gemspec_rz + data = StringIO.new "GET /quick/a-1.gemspec.rz HTTP/1.0\r\n\r\n" + @req.parse data + + @server.quick @req, @res + + assert @res['date'] + assert_equal 'text/plain', @res['content-type'] + yaml = Zlib::Inflate.inflate(@res.body) + assert_match %r|Gem::Specification|, yaml + assert_match %r|name: a|, yaml + assert_match %r|version: "1"|, yaml + end + + def test_quick_z_9_gemspec_rz + data = StringIO.new "GET /quick/z-9.gemspec.rz HTTP/1.0\r\n\r\n" + @req.parse data + + @server.quick @req, @res + + assert_match %r| \d\d:\d\d:\d\d |, @res['date'] + assert_equal 'text/plain', @res['content-type'] + assert_equal '', @res.body + assert_equal 404, @res.status + end + +end + diff --git a/test/rubygems/test_gem_source_index.rb b/test/rubygems/test_gem_source_index.rb new file mode 100644 index 0000000000..befbbe6f67 --- /dev/null +++ b/test/rubygems/test_gem_source_index.rb @@ -0,0 +1,429 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/source_index' +require 'rubygems/config_file' + +class Gem::SourceIndex + public :fetcher, :fetch_bulk_index, :fetch_quick_index, + :find_missing, :gems, :remove_extra, + :update_with_missing, :unzip +end + +class TestGemSourceIndex < RubyGemTestCase + + def setup + super + + util_setup_fake_fetcher + end + + def test_create_from_directory + # TODO + end + + def test_fetcher + assert_equal @fetcher, @source_index.fetcher + end + + def test_fetch_bulk_index_compressed + util_setup_bulk_fetch true + + use_ui @ui do + fetched_index = @source_index.fetch_bulk_index @uri + assert_equal [@gem1.full_name, @gem4.full_name, @gem2.full_name].sort, + fetched_index.gems.map { |n,s| n }.sort + end + + paths = @fetcher.paths + + assert_equal "#{@gem_repo}/Marshal.#{@marshal_version}.Z", paths.shift + + assert paths.empty?, paths.join(', ') + end + + def test_fetch_bulk_index_error + @fetcher.data["#{@gem_repo}/Marshal.#{@marshal_version}.Z"] = proc { raise SocketError } + @fetcher.data["#{@gem_repo}/Marshal.#{@marshal_version}"] = proc { raise SocketError } + @fetcher.data["#{@gem_repo}/yaml.Z"] = proc { raise SocketError } + @fetcher.data["#{@gem_repo}/yaml"] = proc { raise SocketError } + + e = assert_raise Gem::RemoteSourceException do + use_ui @ui do + @source_index.fetch_bulk_index @uri + end + end + + paths = @fetcher.paths + + assert_equal "#{@gem_repo}/Marshal.#{@marshal_version}.Z", paths.shift + assert_equal "#{@gem_repo}/Marshal.#{@marshal_version}", paths.shift + assert_equal "#{@gem_repo}/yaml.Z", paths.shift + assert_equal "#{@gem_repo}/yaml", paths.shift + + assert paths.empty?, paths.join(', ') + + assert_equal 'Error fetching remote gem cache: SocketError', + e.message + end + + def test_fetch_bulk_index_fallback + @fetcher.data["#{@gem_repo}/Marshal.#{@marshal_version}.Z"] = + proc { raise SocketError } + @fetcher.data["#{@gem_repo}/Marshal.#{@marshal_version}"] = + proc { raise SocketError } + @fetcher.data["#{@gem_repo}/yaml.Z"] = proc { raise SocketError } + @fetcher.data["#{@gem_repo}/yaml"] = @source_index.to_yaml + + use_ui @ui do + fetched_index = @source_index.fetch_bulk_index @uri + assert_equal [@gem1.full_name, @gem4.full_name, @gem2.full_name].sort, + fetched_index.gems.map { |n,s| n }.sort + end + + paths = @fetcher.paths + + assert_equal "#{@gem_repo}/Marshal.#{@marshal_version}.Z", paths.shift + assert_equal "#{@gem_repo}/Marshal.#{@marshal_version}", paths.shift + assert_equal "#{@gem_repo}/yaml.Z", paths.shift + assert_equal "#{@gem_repo}/yaml", paths.shift + + assert paths.empty?, paths.join(', ') + end + + def test_fetch_bulk_index_marshal_mismatch + marshal = @source_index.dump + marshal[0] = (Marshal::MAJOR_VERSION - 1).chr + + @fetcher.data["#{@gem_repo}/Marshal.#{@marshal_version}"] = marshal + @fetcher.data["#{@gem_repo}/yaml"] = @source_index.to_yaml + + use_ui @ui do + fetched_index = @source_index.fetch_bulk_index @uri + assert_equal [@gem1.full_name, @gem4.full_name, @gem2.full_name].sort, + fetched_index.gems.map { |n,s| n }.sort + end + + paths = @fetcher.paths + + assert_equal "#{@gem_repo}/Marshal.#{@marshal_version}.Z", paths.shift + assert_equal "#{@gem_repo}/Marshal.#{@marshal_version}", paths.shift + assert_equal "#{@gem_repo}/yaml.Z", paths.shift + assert_equal "#{@gem_repo}/yaml", paths.shift + + assert paths.empty?, paths.join(', ') + end + + def test_fetch_bulk_index_uncompressed + util_setup_bulk_fetch false + use_ui @ui do + fetched_index = @source_index.fetch_bulk_index @uri + assert_equal [@gem1.full_name, @gem4.full_name, @gem2.full_name].sort, + fetched_index.gems.map { |n,s| n }.sort + end + + paths = @fetcher.paths + + assert_equal "#{@gem_repo}/Marshal.#{@marshal_version}.Z", paths.shift + assert_equal "#{@gem_repo}/Marshal.#{@marshal_version}", paths.shift + + assert paths.empty?, paths.join(', ') + end + + def test_fetch_quick_index + quick_index = util_zip @gem_names + @fetcher.data["#{@gem_repo}/quick/index.rz"] = quick_index + + quick_index = @source_index.fetch_quick_index @uri + assert_equal [@gem1.full_name, @gem4.full_name, @gem2.full_name].sort, + quick_index.sort + + paths = @fetcher.paths + + assert_equal "#{@gem_repo}/quick/index.rz", paths.shift + + assert paths.empty?, paths.join(', ') + end + + def test_fetch_quick_index_error + @fetcher.data["#{@gem_repo}/quick/index.rz"] = + proc { raise Exception } + + e = assert_raise Gem::OperationNotSupportedError do + @source_index.fetch_quick_index @uri + end + + assert_equal 'No quick index found: Exception', e.message + + paths = @fetcher.paths + + assert_equal "#{@gem_repo}/quick/index.rz", paths.shift + + assert paths.empty?, paths.join(', ') + end + + def test_find_missing + missing = @source_index.find_missing [@gem3.full_name] + assert_equal [@gem3.full_name], missing + end + + def test_find_missing_none_missing + missing = @source_index.find_missing @gem_names.split + assert_equal [], missing + end + + def test_latest_specs + spec = quick_gem @gem1.name, '0.0.1' + @source_index.add_spec spec + + expected = [ + @gem1.full_name, + @gem2.full_name, + @gem4.full_name, + ].sort + + assert_equal expected, @source_index.latest_specs.map { |s| s.full_name }.sort + end + + def test_outdated + sic = Gem::SourceInfoCache.new + Gem::SourceInfoCache.instance_variable_set :@cache, sic + + assert_equal [], @source_index.outdated + + updated = quick_gem @gem1.name, (@gem1.version.bump) + util_setup_source_info_cache updated + + assert_equal [updated.name], @source_index.outdated + + updated_platform = quick_gem @gem1.name, (updated.version.bump) do |s| + s.platform = Gem::Platform.new 'x86-other_platform1' + end + + util_setup_source_info_cache updated, updated_platform + + assert_equal [updated_platform.name], @source_index.outdated + end + + def test_remove_extra + @source_index.remove_extra [@gem1.full_name] + assert_equal [@gem1.full_name], @source_index.gems.map { |n,s| n } + end + + def test_remove_extra_no_changes + gems = @gem_names.split.sort + @source_index.remove_extra gems + assert_equal gems, @source_index.gems.map { |n,s| n }.sort + end + + def test_search + assert_equal [@gem1, @gem4], @source_index.search("gem_one") + assert_equal [@gem1], @source_index.search("gem_one", "= 0.0.2") + + assert_equal [], @source_index.search("bogusstring") + assert_equal [], @source_index.search("gem_one", "= 3.2.1") + + @a1 = quick_gem 'a', '1' + @a2 = quick_gem 'a', '2' + + source_index = Gem::SourceIndex.new @a1.full_name => @a1, + @a2.full_name => @a2 + + assert_equal [@a1], source_index.search(@a1.name, '= 1') + + r1 = Gem::Requirement.create '= 1' + assert_equal [@a1], source_index.search(@a1.name, r1) + + dep = Gem::Dependency.new @a1.name, r1 + assert_equal [@a1], source_index.search(dep) + end + + def test_search_empty_cache + empty_source_index = Gem::SourceIndex.new({}) + assert_equal [], empty_source_index.search("foo") + end + + def test_search_platform + util_set_arch 'x86-my_platform1' + + a1 = quick_gem 'a', '1' + a1_mine = quick_gem 'a', '1' do |s| + s.platform = Gem::Platform.new 'x86-my_platform1' + end + a1_other = quick_gem 'a', '1' do |s| + s.platform = Gem::Platform.new 'x86-other_platform1' + end + + si = Gem::SourceIndex.new(a1.full_name => a1, a1_mine.full_name => a1_mine, + a1_other.full_name => a1_other) + + dep = Gem::Dependency.new 'a', Gem::Requirement.new('1') + + gems = si.search dep, true + + assert_equal [a1, a1_mine], gems.sort + end + + def test_signature + sig = @source_index.gem_signature('foo-1.2.3') + assert_equal 64, sig.length + assert_match(/^[a-f0-9]{64}$/, sig) + end + + def test_specification + assert_equal @gem1, @source_index.specification(@gem1.full_name) + + assert_nil @source_index.specification("foo-1.2.4") + end + + def test_index_signature + sig = @source_index.index_signature + assert_match(/^[a-f0-9]{64}$/, sig) + end + + def test_unzip + input = "x\234+\316\317MU(I\255(\001\000\021\350\003\232" + assert_equal 'some text', @source_index.unzip(input) + end + + def test_update_bulk + util_setup_bulk_fetch true + + @source_index.gems.replace({}) + assert_equal [], @source_index.gems.keys.sort + + use_ui @ui do + @source_index.update @uri + + assert_equal @gem_names.split, @source_index.gems.keys.sort + end + + paths = @fetcher.paths + + assert_equal "#{@gem_repo}/quick/index.rz", paths.shift + assert_equal "#{@gem_repo}/Marshal.#{@marshal_version}.Z", paths.shift + + assert paths.empty?, paths.join(', ') + end + + def test_update_incremental + old_gem_conf = Gem.configuration + Gem.configuration = Gem::ConfigFile.new([]) + + quick_index = util_zip @all_gem_names.join("\n") + @fetcher.data["#{@gem_repo}/quick/index.rz"] = quick_index + + marshal_uri = File.join @gem_repo, "quick", "Marshal.#{@marshal_version}", + "#{@gem3.full_name}.gemspec.rz" + @fetcher.data[marshal_uri] = util_zip Marshal.dump(@gem3) + + use_ui @ui do + @source_index.update @uri + + assert_equal @all_gem_names, @source_index.gems.keys.sort + end + + paths = @fetcher.paths + assert_equal "#{@gem_repo}/quick/index.rz", paths.shift + assert_equal marshal_uri, paths.shift + + assert paths.empty?, paths.join(', ') + ensure + Gem.configuration = old_gem_conf + end + + def test_update_incremental_fallback + old_gem_conf = Gem.configuration + Gem.configuration = Gem::ConfigFile.new([]) + + quick_index = util_zip @all_gem_names.join("\n") + @fetcher.data["#{@gem_repo}/quick/index.rz"] = quick_index + + marshal_uri = File.join @gem_repo, "quick", "Marshal.#{@marshal_version}", + "#{@gem3.full_name}.gemspec.rz" + + yaml_uri = "#{@gem_repo}/quick/#{@gem3.full_name}.gemspec.rz" + @fetcher.data[yaml_uri] = util_zip @gem3.to_yaml + + use_ui @ui do + @source_index.update @uri + + assert_equal @all_gem_names, @source_index.gems.keys.sort + end + + paths = @fetcher.paths + assert_equal "#{@gem_repo}/quick/index.rz", paths.shift + assert_equal marshal_uri, paths.shift + assert_equal yaml_uri, paths.shift + + assert paths.empty?, paths.join(', ') + ensure + Gem.configuration = old_gem_conf + end + + def test_update_incremental_marshal_mismatch + old_gem_conf = Gem.configuration + Gem.configuration = Gem::ConfigFile.new([]) + + quick_index = util_zip @all_gem_names.join("\n") + @fetcher.data["#{@gem_repo}/quick/index.rz"] = quick_index + + marshal_uri = File.join @gem_repo, "quick", "Marshal.#{@marshal_version}", + "#{@gem3.full_name}.gemspec.rz" + marshal_data = Marshal.dump(@gem3) + marshal_data[0] = (Marshal::MAJOR_VERSION - 1).chr + @fetcher.data[marshal_uri] = util_zip marshal_data + + yaml_uri = "#{@gem_repo}/quick/#{@gem3.full_name}.gemspec.rz" + @fetcher.data[yaml_uri] = util_zip @gem3.to_yaml + + use_ui @ui do + @source_index.update @uri + + assert_equal @all_gem_names, @source_index.gems.keys.sort + end + + paths = @fetcher.paths + assert_equal "#{@gem_repo}/quick/index.rz", paths.shift + assert_equal marshal_uri, paths.shift + assert_equal yaml_uri, paths.shift + + assert paths.empty?, paths.join(', ') + ensure + Gem.configuration = old_gem_conf + end + + def test_update_with_missing + marshal_uri = File.join @gem_repo, "quick", "Marshal.#{@marshal_version}", + "#{@gem3.full_name}.gemspec.rz" + dumped = Marshal.dump(@gem3) + @fetcher.data[marshal_uri] = util_zip(dumped) + + use_ui @ui do + @source_index.update_with_missing @uri, [@gem3.full_name] + end + + spec = @source_index.specification(@gem3.full_name) + # We don't care about the equality of undumped attributes + @gem3.files = spec.files + @gem3.loaded_from = spec.loaded_from + + assert_equal @gem3, spec + end + + def util_setup_bulk_fetch(compressed) + source_index = @source_index.dump + + if compressed then + @fetcher.data["#{@gem_repo}/Marshal.#{@marshal_version}.Z"] = util_zip source_index + else + @fetcher.data["#{@gem_repo}/Marshal.#{@marshal_version}"] = source_index + end + end + +end diff --git a/test/rubygems/test_gem_source_info_cache.rb b/test/rubygems/test_gem_source_info_cache.rb new file mode 100644 index 0000000000..570b643bc5 --- /dev/null +++ b/test/rubygems/test_gem_source_info_cache.rb @@ -0,0 +1,299 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/source_info_cache' + +class Gem::SourceIndex + public :gems +end + +class TestGemSourceInfoCache < RubyGemTestCase + + def setup + @original_sources = Gem.sources + + super + + util_setup_fake_fetcher + + @sic = Gem::SourceInfoCache.new + @sic.instance_variable_set :@fetcher, @fetcher + + prep_cache_files @sic + end + + def teardown + super + Gem.sources.replace @original_sources + end + + def test_self_cache_refreshes + Gem.configuration.update_sources = true #true by default + source_index = Gem::SourceIndex.new 'key' => 'sys' + @fetcher.data["#{@gem_repo}/Marshal.#{@marshal_version}"] = source_index.dump + + Gem.sources.replace %W[#{@gem_repo}] + + use_ui @ui do + assert_not_nil Gem::SourceInfoCache.cache + assert_kind_of Gem::SourceInfoCache, Gem::SourceInfoCache.cache + assert_equal Gem::SourceInfoCache.cache.object_id, + Gem::SourceInfoCache.cache.object_id + assert_match %r|Bulk updating|, @ui.output + end + end + + def test_self_cache_skips_refresh_based_on_configuration + Gem.configuration.update_sources = false + source_index = Gem::SourceIndex.new 'key' => 'sys' + @fetcher.data["#{@gem_repo}/Marshal.#{@marshal_version}"] = source_index.dump + + Gem.sources.replace %w[#{@gem_repo}] + + use_ui @ui do + assert_not_nil Gem::SourceInfoCache.cache + assert_kind_of Gem::SourceInfoCache, Gem::SourceInfoCache.cache + assert_equal Gem::SourceInfoCache.cache.object_id, + Gem::SourceInfoCache.cache.object_id + assert_no_match %r|Bulk updating|, @ui.output + end + end + + def test_self_cache_data + source_index = Gem::SourceIndex.new 'key' => 'sys' + @fetcher.data["#{@gem_repo}/Marshal.#{@marshal_version}"] = source_index.dump + + Gem::SourceInfoCache.instance_variable_set :@cache, nil + sice = Gem::SourceInfoCacheEntry.new source_index, 0 + + use_ui @ui do + assert_equal source_index.gems, + Gem::SourceInfoCache.cache_data[@gem_repo].source_index.gems + end + end + + def test_cache_data + assert_equal [['key','sys']], @sic.cache_data.to_a.sort + end + + def test_cache_data_dirty + def @sic.dirty() @dirty; end + assert_equal false, @sic.dirty, 'clean on init' + @sic.cache_data + assert_equal false, @sic.dirty, 'clean on fetch' + @sic.update + @sic.cache_data + assert_equal true, @sic.dirty, 'still dirty' + end + + def test_cache_data_irreparable + @fetcher.data["#{@gem_repo}/Marshal.#{@marshal_version}"] = @source_index.dump + + data = { @gem_repo => { 'totally' => 'borked' } } + + [@sic.system_cache_file, @sic.user_cache_file].each do |fn| + FileUtils.mkdir_p File.dirname(fn) + open(fn, "wb") { |f| f.write Marshal.dump(data) } + end + + @sic.instance_eval { @cache_data = nil } + + fetched = use_ui @ui do @sic.cache_data end + + fetched_si = fetched["#{@gem_repo}"].source_index + + assert_equal @source_index.index_signature, fetched_si.index_signature + end + + def test_cache_data_none_readable + FileUtils.chmod 0222, @sic.system_cache_file + FileUtils.chmod 0222, @sic.user_cache_file + return if (File.stat(@sic.system_cache_file).mode & 0222) != 0222 + return if (File.stat(@sic.user_cache_file).mode & 0222) != 0222 + # HACK for systems that don't support chmod + assert_equal({}, @sic.cache_data) + end + + def test_cache_data_none_writable + FileUtils.chmod 0444, @sic.system_cache_file + FileUtils.chmod 0444, @sic.user_cache_file + e = assert_raise RuntimeError do + @sic.cache_data + end + assert_equal 'unable to locate a writable cache file', e.message + end + + def test_cache_data_repair + data = { + @gem_repo => { + 'cache' => Gem::SourceIndex.new, + 'size' => 0, + } + } + [@sic.system_cache_file, @sic.user_cache_file].each do |fn| + FileUtils.mkdir_p File.dirname(fn) + open(fn, "wb") { |f| f.write Marshal.dump(data) } + end + + @sic.instance_eval { @cache_data = nil } + + expected = { + @gem_repo => + Gem::SourceInfoCacheEntry.new(Gem::SourceIndex.new, 0) + } + assert_equal expected, @sic.cache_data + end + + def test_cache_data_user_fallback + FileUtils.chmod 0444, @sic.system_cache_file + assert_equal [['key','usr']], @sic.cache_data.to_a.sort + end + + def test_cache_file + assert_equal @gemcache, @sic.cache_file + end + + def test_cache_file_user_fallback + FileUtils.chmod 0444, @sic.system_cache_file + assert_equal @usrcache, @sic.cache_file + end + + def test_cache_file_none_writable + FileUtils.chmod 0444, @sic.system_cache_file + FileUtils.chmod 0444, @sic.user_cache_file + e = assert_raise RuntimeError do + @sic.cache_file + end + assert_equal 'unable to locate a writable cache file', e.message + end + + def test_flush + @sic.cache_data['key'] = 'new' + @sic.update + @sic.flush + + assert_equal [['key','new']], read_cache(@sic.system_cache_file).to_a.sort + end + + def test_read_system_cache + assert_equal [['key','sys']], @sic.cache_data.to_a.sort + end + + def test_read_user_cache + FileUtils.chmod 0444, @sic.system_cache_file + + assert_equal [['key','usr']], @sic.cache_data.to_a.sort + end + + def test_search + si = Gem::SourceIndex.new @gem1.full_name => @gem1 + cache_data = { + @gem_repo => Gem::SourceInfoCacheEntry.new(si, nil) + } + @sic.instance_variable_set :@cache_data, cache_data + + assert_equal [@gem1], @sic.search(//) + end + + def test_search_dependency + si = Gem::SourceIndex.new @gem1.full_name => @gem1 + cache_data = { + @gem_repo => Gem::SourceInfoCacheEntry.new(si, nil) + } + @sic.instance_variable_set :@cache_data, cache_data + + dep = Gem::Dependency.new @gem1.name, @gem1.version + + assert_equal [@gem1], @sic.search(dep) + end + + def test_search_no_matches + si = Gem::SourceIndex.new @gem1.full_name => @gem1 + cache_data = { + @gem_repo => Gem::SourceInfoCacheEntry.new(si, nil) + } + @sic.instance_variable_set :@cache_data, cache_data + + assert_equal [], @sic.search(/nonexistent/) + end + + def test_search_no_matches_in_source + si = Gem::SourceIndex.new @gem1.full_name => @gem1 + cache_data = { + @gem_repo => Gem::SourceInfoCacheEntry.new(si, nil) + } + @sic.instance_variable_set :@cache_data, cache_data + Gem.sources.replace %w[more-gems.example.com] + + assert_equal [], @sic.search(/nonexistent/) + end + + def test_search_with_source + si = Gem::SourceIndex.new @gem1.full_name => @gem1 + cache_data = { + @gem_repo => Gem::SourceInfoCacheEntry.new(si, nil) + } + @sic.instance_variable_set :@cache_data, cache_data + + assert_equal [[@gem1, @gem_repo]], + @sic.search_with_source(//) + end + + def test_system_cache_file + assert_equal File.join(Gem.dir, "source_cache"), @sic.system_cache_file + end + + def test_user_cache_file + assert_equal @usrcache, @sic.user_cache_file + end + + def test_write_cache + @sic.cache_data['key'] = 'new' + @sic.write_cache + + assert_equal [['key', 'new']], + read_cache(@sic.system_cache_file).to_a.sort + assert_equal [['key', 'usr']], + read_cache(@sic.user_cache_file).to_a.sort + end + + def test_write_cache_user + FileUtils.chmod 0444, @sic.system_cache_file + @sic.set_cache_data({'key' => 'new'}) + @sic.update + @sic.write_cache + + assert_equal [['key', 'sys']], read_cache(@sic.system_cache_file).to_a.sort + assert_equal [['key', 'new']], read_cache(@sic.user_cache_file).to_a.sort + end + + def test_write_cache_user_from_scratch + FileUtils.rm_rf @sic.user_cache_file + FileUtils.chmod 0444, @sic.system_cache_file + @sic.set_cache_data({'key' => 'new'}) + @sic.update + @sic.write_cache + + assert_equal [['key', 'sys']], read_cache(@sic.system_cache_file).to_a.sort + assert_equal [['key', 'new']], read_cache(@sic.user_cache_file).to_a.sort + end + + def test_write_cache_user_no_directory + FileUtils.rm_rf File.dirname(@sic.user_cache_file) + FileUtils.chmod 0444, @sic.system_cache_file + @sic.set_cache_data({'key' => 'new'}) + @sic.update + @sic.write_cache + + assert_equal [['key','sys']], read_cache(@sic.system_cache_file).to_a.sort + assert_equal [['key','new']], read_cache(@sic.user_cache_file).to_a.sort + end + +end + diff --git a/test/rubygems/test_gem_source_info_cache_entry.rb b/test/rubygems/test_gem_source_info_cache_entry.rb new file mode 100644 index 0000000000..023baf948b --- /dev/null +++ b/test/rubygems/test_gem_source_info_cache_entry.rb @@ -0,0 +1,46 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/source_info_cache_entry' + +class TestGemSourceInfoCacheEntry < RubyGemTestCase + + def setup + super + + util_setup_fake_fetcher + + @si = Gem::SourceIndex.new @gem1.full_name => @gem1.name + @sic_e = Gem::SourceInfoCacheEntry.new @si, @si.dump.size + end + + def test_refresh + @fetcher.data["#{@gem_repo}/Marshal.#{@marshal_version}.Z"] = + proc { raise Exception } + @fetcher.data["#{@gem_repo}/Marshal.#{@marshal_version}"] = @si.dump + + assert_nothing_raised do + @sic_e.refresh @gem_repo + end + end + + def test_refresh_bad_uri + assert_raise URI::BadURIError do + @sic_e.refresh 'gems.example.com' + end + end + + def test_refresh_update + si = Gem::SourceIndex.new @gem1.full_name => @gem1, + @gem2.full_name => @gem2 + @fetcher.data["#{@gem_repo}/Marshal.#{@marshal_version}"] = si.dump + + use_ui @ui do + @sic_e.refresh @gem_repo + end + + new_gem = @sic_e.source_index.specification(@gem2.full_name) + assert_equal @gem2.full_name, new_gem.full_name + end + +end + diff --git a/test/rubygems/test_gem_specification.rb b/test/rubygems/test_gem_specification.rb new file mode 100644 index 0000000000..96db29c2c0 --- /dev/null +++ b/test/rubygems/test_gem_specification.rb @@ -0,0 +1,707 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'stringio' +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/specification' + +class TestGemSpecification < RubyGemTestCase + + LEGACY_YAML_SPEC = <<-EOF +--- !ruby/object:Gem::Specification +rubygems_version: "1.0" +name: keyedlist +version: !ruby/object:Gem::Version + version: 0.4.0 +date: 2004-03-28 15:37:49.828000 +02:00 +platform: +summary: A Hash which automatically computes keys. +require_paths: + - lib +files: + - lib/keyedlist.rb +autorequire: keyedlist +author: Florian Gross +email: flgr@ccan.de +has_rdoc: true + EOF + + LEGACY_RUBY_SPEC = <<-EOF +Gem::Specification.new do |s| + s.name = %q{keyedlist} + s.version = %q{0.4.0} + s.has_rdoc = true + s.summary = %q{A Hash which automatically computes keys.} + s.files = ["lib/keyedlist.rb"] + s.require_paths = ["lib"] + s.autorequire = %q{keyedlist} + s.author = %q{Florian Gross} + s.email = %q{flgr@ccan.de} +end + EOF + + def setup + super + + @a0_0_1 = quick_gem 'a', '0.0.1' do |s| + s.executable = 'exec' + s.extensions << 'ext/a/extconf.rb' + s.has_rdoc = 'true' + s.test_file = 'test/suite.rb' + s.requirements << 'A working computer' + + s.add_dependency 'rake', '> 0.4' + s.add_dependency 'jabber4r', '> 0.0.0' + s.add_dependency 'pqa', ['> 0.4', '<= 0.6'] + + s.mark_version + s.files = %w[lib/code.rb] + end + + @a0_0_2 = quick_gem 'a', '0.0.2' do |s| + s.files = %w[lib/code.rb] + end + end + + def test_self_attribute_names + expected_value = %w[ + authors + autorequire + bindir + cert_chain + date + default_executable + dependencies + description + email + executables + extensions + extra_rdoc_files + files + has_rdoc + homepage + name + platform + post_install_message + rdoc_options + require_paths + required_ruby_version + required_rubygems_version + requirements + rubyforge_project + rubygems_version + signing_key + specification_version + summary + test_files + version + ] + + actual_value = Gem::Specification.attribute_names.map { |a| a.to_s }.sort + + assert_equal expected_value, actual_value + end + + def test_self_load + spec = File.join @gemhome, 'specifications', "#{@a0_0_2.full_name}.gemspec" + gs = Gem::Specification.load spec + + assert_equal @a0_0_2, gs + end + + def test_self_load_legacy_ruby + s = eval LEGACY_RUBY_SPEC + assert_equal 'keyedlist', s.name + assert_equal '0.4.0', s.version.to_s + assert_equal true, s.has_rdoc? + assert_equal Gem::Specification::TODAY, s.date + assert s.required_ruby_version.satisfied_by?(Gem::Version.new('0.0.1')) + assert_equal false, s.has_unit_tests? + end + + def test_self_load_legacy_yaml + s = YAML.load StringIO.new(LEGACY_YAML_SPEC) + assert_equal 'keyedlist', s.name + assert_equal '0.4.0', s.version.to_s + assert_equal true, s.has_rdoc? + #assert_equal Date.today, s.date + #assert s.required_ruby_version.satisfied_by?(Gem::Version.new('0.0.1')) + assert_equal false, s.has_unit_tests? + end + + def test_self_normalize_yaml_input_with_183_yaml + input = "!ruby/object:Gem::Specification " + assert_equal "--- #{input}", Gem::Specification.normalize_yaml_input(input) + end + + def test_self_normalize_yaml_input_with_non_183_yaml + input = "--- !ruby/object:Gem::Specification " + assert_equal input, Gem::Specification.normalize_yaml_input(input) + end + + def test_self_normalize_yaml_input_with_183_io + input = "!ruby/object:Gem::Specification " + assert_equal "--- #{input}", + Gem::Specification.normalize_yaml_input(StringIO.new(input)) + end + + def test_self_normalize_yaml_input_with_non_183_io + input = "--- !ruby/object:Gem::Specification " + assert_equal input, + Gem::Specification.normalize_yaml_input(StringIO.new(input)) + end + + def test_initialize + spec = Gem::Specification.new do |s| + s.name = "blah" + s.version = "1.3.5" + end + + assert_equal "blah", spec.name + assert_equal "1.3.5", spec.version.to_s + assert_equal Gem::Platform::RUBY, spec.platform + assert_equal nil, spec.summary + assert_equal [], spec.files + + assert_equal [], spec.test_files + assert_equal [], spec.rdoc_options + assert_equal [], spec.extra_rdoc_files + assert_equal [], spec.executables + assert_equal [], spec.extensions + assert_equal [], spec.requirements + assert_equal [], spec.dependencies + assert_equal 'bin', spec.bindir + assert_equal false, spec.has_rdoc + assert_equal false, spec.has_rdoc? + assert_equal '>= 0', spec.required_ruby_version.to_s + assert_equal '>= 0', spec.required_rubygems_version.to_s + end + + def test_initialize_future + version = Gem::Specification::CURRENT_SPECIFICATION_VERSION + 1 + spec = Gem::Specification.new do |s| + s.name = "blah" + s.version = "1.3.5" + + s.specification_version = version + + s.new_unknown_attribute = "a value" + end + + assert_equal "blah", spec.name + assert_equal "1.3.5", spec.version.to_s + end + + def test_author + assert_equal 'A User', @a0_0_1.author + end + + def test_authors + assert_equal ['A User'], @a0_0_1.authors + end + + def test_bindir_equals + @a0_0_1.bindir = 'apps' + + assert_equal 'apps', @a0_0_1.bindir + end + + def test_bindir_equals_nil + @a0_0_2.bindir = nil + @a0_0_2.executable = 'app' + + assert_equal nil, @a0_0_2.bindir + assert_equal %w[lib/code.rb app], @a0_0_2.files + end + + def test_date + assert_equal Gem::Specification::TODAY, @a0_0_1.date + end + + def test_date_equals_date + @a0_0_1.date = Date.new(2003, 9, 17) + assert_equal Time.local(2003, 9, 17, 0,0,0), @a0_0_1.date + end + + def test_date_equals_string + @a0_0_1.date = '2003-09-17' + assert_equal Time.local(2003, 9, 17, 0,0,0), @a0_0_1.date + end + + def test_date_equals_time + @a0_0_1.date = Time.local(2003, 9, 17, 0,0,0) + assert_equal Time.local(2003, 9, 17, 0,0,0), @a0_0_1.date + end + + def test_date_equals_time_local + # HACK PDT + @a0_0_1.date = Time.local(2003, 9, 17, 19,50,0) + assert_equal Time.local(2003, 9, 17, 0,0,0), @a0_0_1.date + end + + def test_date_equals_time_utc + # HACK PDT + @a0_0_1.date = Time.local(2003, 9, 17, 19,50,0) + assert_equal Time.local(2003, 9, 17, 0,0,0), @a0_0_1.date + end + + def test_default_executable + assert_equal 'exec', @a0_0_1.default_executable + + @a0_0_1.default_executable = nil + @a0_0_1.instance_variable_set :@executables, nil + assert_equal nil, @a0_0_1.default_executable + end + + def test_dependencies + rake = Gem::Dependency.new 'rake', '> 0.4' + jabber = Gem::Dependency.new 'jabber4r', '> 0.0.0' + pqa = Gem::Dependency.new 'pqa', ['> 0.4', '<= 0.6'] + + assert_equal [rake, jabber, pqa], @a0_0_1.dependencies + end + + def test_description + assert_equal 'This is a test description', @a0_0_1.description + end + + def test_eql_eh + g1 = quick_gem 'gem' + g2 = quick_gem 'gem' + + assert_equal g1, g2 + assert_equal g1.hash, g2.hash + assert_equal true, g1.eql?(g2) + end + + def test_equals2 + assert_equal @a0_0_1, @a0_0_1 + assert_equal @a0_0_1, @a0_0_1.dup + assert_not_equal @a0_0_1, @a0_0_2 + assert_not_equal @a0_0_1, Object.new + end + + # The cgikit specification was reported to be causing trouble in at least + # one version of RubyGems, so we test explicitly for it. + def test_equals2_cgikit + cgikit = Gem::Specification.new do |s| + s.name = %q{cgikit} + s.version = "1.1.0" + s.date = %q{2004-03-13} + s.summary = %q{CGIKit is a componented-oriented web application } + + %q{framework like Apple Computers WebObjects. } + + %{This framework services Model-View-Controller architecture } + + %q{programming by components based on a HTML file, a definition } + + %q{file and a Ruby source. } + s.email = %q{info@spice-of-life.net} + s.homepage = %q{http://www.spice-of-life.net/download/cgikit/} + s.autorequire = %q{cgikit} + s.bindir = nil + s.has_rdoc = nil + s.required_ruby_version = nil + s.platform = nil + s.files = ["lib/cgikit", "lib/cgikit.rb", "lib/cgikit/components", "..."] + end + + assert_equal cgikit, cgikit + end + + def test_equals2_default_executable + spec = @a0_0_1.dup + spec.default_executable = 'xx' + + assert_not_equal @a0_0_1, spec + assert_not_equal spec, @a0_0_1 + end + + def test_equals2_extensions + spec = @a0_0_1.dup + spec.extensions = 'xx' + + assert_not_equal @a0_0_1, spec + assert_not_equal spec, @a0_0_1 + end + + def test_executables + @a0_0_1.executable = 'app' + assert_equal %w[app], @a0_0_1.executables + end + + def test_executable_equals + @a0_0_2.executable = 'app' + assert_equal 'app', @a0_0_2.executable + assert_equal %w[lib/code.rb bin/app], @a0_0_2.files + end + + def test_extensions + assert_equal ['ext/a/extconf.rb'], @a0_0_1.extensions + end + + def test_files + @a0_0_1.files = %w(files bin/common) + @a0_0_1.test_files = %w(test_files bin/common) + @a0_0_1.executables = %w(executables common) + @a0_0_1.extra_rdoc_files = %w(extra_rdoc_files bin/common) + @a0_0_1.extensions = %w(extensions bin/common) + + expected = %w[ + bin/common + bin/executables + extensions + extra_rdoc_files + files + test_files + ] + assert_equal expected, @a0_0_1.files.sort + end + + def test_files_duplicate + @a0_0_2.files = %w[a b c d b] + @a0_0_2.extra_rdoc_files = %w[x y z x] + @a0_0_2.normalize + + assert_equal %w[a b c d x y z], @a0_0_2.files + assert_equal %w[x y z], @a0_0_2.extra_rdoc_files + end + + def test_files_extra_rdoc_files + @a0_0_2.files = %w[a b c d] + @a0_0_2.extra_rdoc_files = %w[x y z] + @a0_0_2.normalize + assert_equal %w[a b c d x y z], @a0_0_2.files + end + + def test_files_non_array + @a0_0_1.files = "F" + @a0_0_1.test_files = "TF" + @a0_0_1.executables = "X" + @a0_0_1.extra_rdoc_files = "ERF" + @a0_0_1.extensions = "E" + + assert_equal %w[E ERF F TF bin/X], @a0_0_1.files.sort + end + + def test_files_non_array_pathological + @a0_0_1.instance_variable_set :@files, "F" + @a0_0_1.instance_variable_set :@test_files, "TF" + @a0_0_1.instance_variable_set :@extra_rdoc_files, "ERF" + @a0_0_1.instance_variable_set :@extensions, "E" + @a0_0_1.instance_variable_set :@executables, "X" + + assert_equal %w[E ERF F TF bin/X], @a0_0_1.files.sort + assert_kind_of Integer, @a0_0_1.hash + end + + def test_full_name + assert_equal 'a-0.0.1', @a0_0_1.full_name + + @a0_0_1.platform = Gem::Platform.new ['universal', 'darwin', nil] + assert_equal 'a-0.0.1-universal-darwin', @a0_0_1.full_name + + @a0_0_1.instance_variable_set :@new_platform, 'mswin32' + assert_equal 'a-0.0.1-mswin32', @a0_0_1.full_name, 'legacy' + + return if win_platform? + + @a0_0_1.platform = 'current' + assert_equal 'a-0.0.1-x86-darwin-8', @a0_0_1.full_name + end + + def test_full_name_windows + test_cases = { + 'i386-mswin32' => 'a-0.0.1-x86-mswin32-60', + 'i386-mswin32_80' => 'a-0.0.1-x86-mswin32-80', + 'i386-mingw32' => 'a-0.0.1-x86-mingw32' + } + + test_cases.each do |arch, expected| + util_set_arch arch + @a0_0_1.platform = 'current' + assert_equal expected, @a0_0_1.full_name + end + end + + def test_has_rdoc_eh + assert_equal true, @a0_0_1.has_rdoc? + end + + def test_hash + assert_equal @a0_0_1.hash, @a0_0_1.hash + assert_equal @a0_0_1.hash, @a0_0_1.dup.hash + assert_not_equal @a0_0_1.hash, @a0_0_2.hash + end + + def test_lib_files + @a0_0_1.files = %w[lib/foo.rb Rakefile] + + assert_equal %w[lib/foo.rb], @a0_0_1.lib_files + end + + def test_name + assert_equal 'a', @a0_0_1.name + end + + def test_platform + assert_equal Gem::Platform::RUBY, @a0_0_1.platform + end + + def test_platform_equals + @a0_0_1.platform = nil + assert_equal Gem::Platform::RUBY, @a0_0_1.platform + + @a0_0_1.platform = Gem::Platform::RUBY + assert_equal Gem::Platform::RUBY, @a0_0_1.platform + + test_cases = { + 'i386-mswin32' => ['x86', 'mswin32', '60'], + 'i386-mswin32_80' => ['x86', 'mswin32', '80'], + 'i386-mingw32' => ['x86', 'mingw32', nil ], + 'x86-darwin8' => ['x86', 'darwin', '8' ], + } + + test_cases.each do |arch, expected| + util_set_arch arch + @a0_0_1.platform = Gem::Platform::CURRENT + assert_equal Gem::Platform.new(expected), @a0_0_1.platform + end + end + + def test_platform_equals_legacy + @a0_0_1.platform = Gem::Platform::WIN32 + assert_equal Gem::Platform::MSWIN32, @a0_0_1.platform + + @a0_0_1.platform = Gem::Platform::LINUX_586 + assert_equal Gem::Platform::X86_LINUX, @a0_0_1.platform + + @a0_0_1.platform = Gem::Platform::DARWIN + assert_equal Gem::Platform::PPC_DARWIN, @a0_0_1.platform + end + + def test_require_paths + @a0_0_1.require_path = 'lib' + assert_equal %w[lib], @a0_0_1.require_paths + end + + def test_requirements + assert_equal ['A working computer'], @a0_0_1.requirements + end + + def test_spaceship_name + s1 = quick_gem 'a', '1' + s2 = quick_gem 'b', '1' + + assert_equal(-1, (s1 <=> s2)) + assert_equal( 0, (s1 <=> s1)) + assert_equal( 1, (s2 <=> s1)) + end + + def test_spaceship_platform + s1 = quick_gem 'a', '1' + s2 = quick_gem 'a', '1' do |s| + s.platform = Gem::Platform.new 'x86-my_platform1' + end + + assert_equal( -1, (s1 <=> s2)) + assert_equal( 0, (s1 <=> s1)) + assert_equal( 1, (s2 <=> s1)) + end + + def test_spaceship_version + s1 = quick_gem 'a', '1' + s2 = quick_gem 'a', '2' + + assert_equal( -1, (s1 <=> s2)) + assert_equal( 0, (s1 <=> s1)) + assert_equal( 1, (s2 <=> s1)) + end + + def test_summary + assert_equal 'this is a summary', @a0_0_1.summary + end + + def test_test_files + @a0_0_1.test_file = 'test/suite.rb' + assert_equal ['test/suite.rb'], @a0_0_1.test_files + end + + def test_test_suite_file + @a0_0_2.test_suite_file = 'test/suite.rb' + assert_equal ['test/suite.rb'], @a0_0_2.test_files + # XXX: what about the warning? + end + + def test_to_ruby + @a0_0_2.required_rubygems_version = Gem::Requirement.new '> 0' + + ruby_code = @a0_0_2.to_ruby + + expected = "Gem::Specification.new do |s| + s.name = %q{a} + s.version = \"0.0.2\" + + s.specification_version = #{Gem::Specification::CURRENT_SPECIFICATION_VERSION} if s.respond_to? :specification_version= + + s.required_rubygems_version = Gem::Requirement.new(\"> 0\") if s.respond_to? :required_rubygems_version= + s.authors = [\"A User\"] + s.date = %q{#{Gem::Specification::TODAY.strftime "%Y-%m-%d"}} + s.description = %q{This is a test description} + s.email = %q{example@example.com} + s.files = [\"lib/code.rb\"] + s.has_rdoc = true + s.homepage = %q{http://example.com} + s.require_paths = [\"lib\"] + s.rubygems_version = %q{#{Gem::RubyGemsVersion}} + s.summary = %q{this is a summary} +end +" + + assert_equal expected, ruby_code + + same_spec = eval ruby_code + + assert_equal @a0_0_2, same_spec + end + + def test_to_ruby_fancy + @a0_0_1.platform = Gem::Platform::PPC_DARWIN + ruby_code = @a0_0_1.to_ruby + + expected = "Gem::Specification.new do |s| + s.name = %q{a} + s.version = \"0.0.1\" + + s.specification_version = 2 if s.respond_to? :specification_version= + + s.required_rubygems_version = Gem::Requirement.new(\">= 0\") if s.respond_to? :required_rubygems_version= + s.authors = [\"A User\"] + s.date = %q{#{Gem::Specification::TODAY.strftime "%Y-%m-%d"}} + s.default_executable = %q{exec} + s.description = %q{This is a test description} + s.email = %q{example@example.com} + s.executables = [\"exec\"] + s.extensions = [\"ext/a/extconf.rb\"] + s.files = [\"lib/code.rb\", \"test/suite.rb\", \"bin/exec\", \"ext/a/extconf.rb\"] + s.has_rdoc = %q{true} + s.homepage = %q{http://example.com} + s.platform = Gem::Platform.new([\"ppc\", \"darwin\", nil]) + s.require_paths = [\"lib\"] + s.requirements = [\"A working computer\"] + s.rubygems_version = %q{0.9.4.6} + s.summary = %q{this is a summary} + s.test_files = [\"test/suite.rb\"] + + s.add_dependency(%q, [\"> 0.4\"]) + s.add_dependency(%q, [\"> 0.0.0\"]) + s.add_dependency(%q, [\"> 0.4\", \"<= 0.6\"]) +end +" + + assert_equal expected, ruby_code + + same_spec = eval ruby_code + + assert_equal @a0_0_1, same_spec + end + + def test_to_ruby_legacy + gemspec1 = eval LEGACY_RUBY_SPEC + ruby_code = gemspec1.to_ruby + gemspec2 = eval ruby_code + + assert_equal gemspec1, gemspec2 + end + + def test_to_yaml + yaml_str = @a0_0_1.to_yaml + same_spec = YAML.load(yaml_str) + + assert_equal @a0_0_1, same_spec + end + + def test_to_yaml_fancy + @a0_0_1.platform = Gem::Platform::PPC_DARWIN + yaml_str = @a0_0_1.to_yaml + + same_spec = YAML.load(yaml_str) + + assert_equal Gem::Platform::PPC_DARWIN, same_spec.platform + + assert_equal @a0_0_1, same_spec + end + + def test_to_yaml_legacy_platform + @a0_0_1.platform = 'powerpc-darwin7.9.0' + + yaml_str = @a0_0_1.to_yaml + + same_spec = YAML.load(yaml_str) + + assert_equal Gem::Platform.new('powerpc-darwin7'), same_spec.platform + assert_equal 'powerpc-darwin7.9.0', same_spec.original_platform + end + + def test_validate + assert @a0_0_1.validate + end + + def test_validate_empty + e = assert_raise Gem::InvalidSpecificationException do + Gem::Specification.new.validate + end + + assert_equal 'missing value for attribute name', e.message + end + + def test_validate_empty_require_paths + @a0_0_1.require_paths = [] + e = assert_raise Gem::InvalidSpecificationException do + @a0_0_1.validate + end + + assert_equal 'specification must have at least one require_path', e.message + end + + def test_validate_platform_bad + @a0_0_1.platform = Object.new + assert_raise Gem::InvalidSpecificationException do @a0_0_1.validate end + + @a0_0_1.platform = "my-custom-platform" + e = assert_raise Gem::InvalidSpecificationException do + @a0_0_1.validate + end + + assert_equal 'invalid platform "my-custom-platform", see Gem::Platform', + e.message + end + + def test_validate_platform_legacy + @a0_0_1.platform = Gem::Platform::WIN32 + assert @a0_0_1.validate + + @a0_0_1.platform = Gem::Platform::LINUX_586 + assert @a0_0_1.validate + + @a0_0_1.platform = Gem::Platform::DARWIN + assert @a0_0_1.validate + end + + def test_validate_rubygems_version + @a0_0_1.rubygems_version = "3" + e = assert_raise Gem::InvalidSpecificationException do + @a0_0_1.validate + end + + assert_equal "expected RubyGems version #{Gem::RubyGemsVersion}, was 3", + e.message + end + + def test_version + assert_equal Gem::Version.new('0.0.1'), @a0_0_1.version + end + +end + diff --git a/test/rubygems/test_gem_stream_ui.rb b/test/rubygems/test_gem_stream_ui.rb new file mode 100644 index 0000000000..a8564ba94f --- /dev/null +++ b/test/rubygems/test_gem_stream_ui.rb @@ -0,0 +1,117 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/user_interaction' + +class TestGemStreamUI < RubyGemTestCase + + module IsTty + attr_accessor :tty + + def tty? + @tty = true unless defined? @tty + return @tty + end + + alias_method :isatty, :tty? + end + + def setup + super + + @cfg = Gem.configuration + + @in = StringIO.new + @out = StringIO.new + @err = StringIO.new + + @in.extend IsTty + + @sui = Gem::StreamUI.new @in, @out, @err + end + + def test_ask + timeout(1) do + expected_answer = "Arthur, King of the Britons" + @in.string = "#{expected_answer}\n" + actual_answer = @sui.ask("What is your name?") + assert_equal expected_answer, actual_answer + end + end + + def test_ask_no_tty + @in.tty = false + + timeout(0.1) do + answer = @sui.ask("what is your favorite color?") + assert_equal nil, answer + end + end + + def test_ask_yes_no_no_tty_with_default + @in.tty = false + + timeout(0.1) do + answer = @sui.ask_yes_no("do coconuts migrate?", false) + assert_equal false, answer + + answer = @sui.ask_yes_no("do coconuts migrate?", true) + assert_equal true, answer + end + end + + def test_ask_yes_no_no_tty_without_default + @in.tty = false + + timeout(0.1) do + assert_raises(Gem::OperationNotSupportedError) do + @sui.ask_yes_no("do coconuts migrate?") + end + end + end + + def test_choose_from_list + @in.puts "1" + @in.rewind + + result = @sui.choose_from_list 'which one?', %w[foo bar] + + assert_equal ['foo', 0], result + assert_equal "which one?\n 1. foo\n 2. bar\n> ", @out.string + end + + def test_choose_from_list_EOF + result = @sui.choose_from_list 'which one?', %w[foo bar] + + assert_equal [nil, nil], result + assert_equal "which one?\n 1. foo\n 2. bar\n> ", @out.string + end + + def test_proress_reporter_silent_nil + @cfg.verbose = nil + reporter = @sui.progress_reporter 10, 'hi' + assert_kind_of Gem::StreamUI::SilentProgressReporter, reporter + end + + def test_proress_reporter_silent_false + @cfg.verbose = false + reporter = @sui.progress_reporter 10, 'hi' + assert_kind_of Gem::StreamUI::SilentProgressReporter, reporter + assert_equal "", @out.string + end + + def test_proress_reporter_simple + @cfg.verbose = true + reporter = @sui.progress_reporter 10, 'hi' + assert_kind_of Gem::StreamUI::SimpleProgressReporter, reporter + assert_equal "hi\n", @out.string + end + + def test_proress_reporter_verbose + @cfg.verbose = 0 + reporter = @sui.progress_reporter 10, 'hi' + assert_kind_of Gem::StreamUI::VerboseProgressReporter, reporter + assert_equal "hi\n", @out.string + end + +end + diff --git a/test/rubygems/test_gem_validator.rb b/test/rubygems/test_gem_validator.rb new file mode 100644 index 0000000000..c910cfdf11 --- /dev/null +++ b/test/rubygems/test_gem_validator.rb @@ -0,0 +1,70 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require File.join(File.expand_path(File.dirname(__FILE__)), 'simple_gem') +require 'rubygems/validator' + +class TestGemValidator < RubyGemTestCase + + def setup + super + + @simple_gem = SIMPLE_GEM + @validator = Gem::Validator.new + end + + def test_verify_gem_file + gem_file = File.join @tempdir, 'simple_gem.gem' + File.open gem_file, 'wb' do |fp| fp.write @simple_gem end + + assert_equal nil, @validator.verify_gem_file(gem_file) + end + + def test_verify_gem_file_empty + e = assert_raise Gem::VerificationError do + @validator.verify_gem_file '' + end + + assert_equal 'missing gem file ', e.message + end + + def test_verify_gem_file_nonexistent + file = '/nonexistent/nonexistent.gem' + e = assert_raise Gem::VerificationError do + @validator.verify_gem_file file + end + + assert_equal "missing gem file #{file}", e.message + end + + def test_verify_gem + assert_equal nil, @validator.verify_gem(@simple_gem) + end + + def test_verify_gem_empty + e = assert_raise Gem::VerificationError do + @validator.verify_gem '' + end + + assert_equal 'empty gem file', e.message + end + + def test_verify_gem_invalid_checksum + e = assert_raise Gem::VerificationError do + @validator.verify_gem @simple_gem.upcase + end + + assert_equal 'invalid checksum for gem file', e.message + end + + def test_verify_gem_no_sum + assert_equal nil, @validator.verify_gem('words') + end + +end + diff --git a/test/rubygems/test_gem_version.rb b/test/rubygems/test_gem_version.rb new file mode 100644 index 0000000000..95f37ddd23 --- /dev/null +++ b/test/rubygems/test_gem_version.rb @@ -0,0 +1,191 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/version' + +class TestGemVersion < RubyGemTestCase + + def setup + super + + @v1_0 = Gem::Version.new '1.0' + @v1_2 = Gem::Version.new '1.2' + @v1_3 = Gem::Version.new '1.3' + end + + def test_class_create + assert_version Gem::Version.create('1.0') + assert_version Gem::Version.create("1.0 ") + assert_version Gem::Version.create(" 1.0 ") + assert_version Gem::Version.create("1.0\n") + assert_version Gem::Version.create("\n1.0\n") + + assert_equal Gem::Version.create('1'), Gem::Version.create(1) + end + + def test_class_create_malformed + e = assert_raise ArgumentError do Gem::Version.create("junk") end + assert_equal "Malformed version number string junk", e.message + + e = assert_raise ArgumentError do Gem::Version.create("1.0\n2.0") end + assert_equal "Malformed version number string 1.0\n2.0", e.message + end + + def test_bad + assert_inadequate( "", "> 0.1") + assert_inadequate( "1.2.3", "!= 1.2.3") + assert_inadequate( "1.2.003.0.0", "!= 1.02.3") + assert_inadequate( "4.5.6", "< 1.2.3") + assert_inadequate( "1.0", "> 1.1") + assert_inadequate( "0", ">") + assert_inadequate( "0", "<") + assert_inadequate( "", "= 0.1") + assert_inadequate( "1.1.1", "> 1.1.1") + assert_inadequate( "1.2", "= 1.1") + assert_inadequate( "1.40", "= 1.1") + assert_inadequate( "1.3", "= 1.40") + assert_inadequate( "9.3.3", "<= 9.3.2") + assert_inadequate( "9.3.1", ">= 9.3.2") + assert_inadequate( "9.3.03", "<= 9.3.2") + assert_inadequate( "1.0.0.1", "= 1.0") + end + + def test_bump + v = Gem::Version.new("5.2.4") + assert_equal "5.3", v.bump.to_s + end + + def test_bump_one_level + v = Gem::Version.new("5") + assert_equal "6", v.bump.to_s + end + + def test_equals2 + v = Gem::Version.new("1.2") + + assert_equal v, @v1_2 + assert_equal @v1_2, v + + assert_not_equal @v1_2, @v1_3 + assert_not_equal @v1_3, @v1_2 + end + + def test_hash + v = Gem::Version.new("1.2") + assert_equal v.hash, @v1_2.hash + assert_not_equal @v1_2.hash, @v1_3.hash + end + + def test_illformed_requirements + [ ">>> 1.3.5", "> blah" ].each do |rq| + assert_raises(ArgumentError, "req [#{rq}] should fail") { + Gem::Version::Requirement.new(rq) + } + end + end + + def test_normalize + assert_equal [1], Gem::Version.new("1").to_ints + assert_equal [1], Gem::Version.new("1.0").to_ints + assert_equal [1, 1], Gem::Version.new("1.1").to_ints + end + + def test_ok + assert_adequate( "0.2.33", "= 0.2.33") + assert_adequate( "0.2.34", "> 0.2.33") + assert_adequate( "1.0", "= 1.0") + assert_adequate( "1.0", "1.0") + assert_adequate( "1.8.2", "> 1.8.0") + assert_adequate( "1.112", "> 1.111") + assert_adequate( "0.2", "> 0.0.0") + assert_adequate( "0.0.0.0.0.2", "> 0.0.0") + assert_adequate( "0.0.1.0", "> 0.0.0.1") + assert_adequate( "10.3.2", "> 9.3.2") + assert_adequate( "1.0.0.0", "= 1.0") + assert_adequate( "10.3.2", "!= 9.3.4") + assert_adequate( "10.3.2", "> 9.3.2") + assert_adequate( "10.3.2", "> 9.3.2") + assert_adequate( " 9.3.2", ">= 9.3.2") + assert_adequate( "9.3.2 ", ">= 9.3.2") + assert_adequate( "", "= 0") + assert_adequate( "", "< 0.1") + assert_adequate( " ", "< 0.1 ") + assert_adequate( "", " < 0.1") + assert_adequate( "0", "=") + assert_adequate( "0", ">=") + assert_adequate( "0", "<=") + end + + def test_satisfied_by_eh_boxed + assert_inadequate("1.3", "~> 1.4") + assert_adequate( "1.4", "~> 1.4") + assert_adequate( "1.5", "~> 1.4") + assert_inadequate("2.0", "~> 1.4") + + assert_inadequate("1.3", "~> 1.4.4") + assert_inadequate("1.4", "~> 1.4.4") + assert_adequate( "1.4.4", "~> 1.4.4") + assert_adequate( "1.4.5", "~> 1.4.4") + assert_inadequate("1.5", "~> 1.4.4") + assert_inadequate("2.0", "~> 1.4.4") + end + + def test_satisfied_by_eh_multiple + req = [">= 1.4", "<= 1.6", "!= 1.5"] + assert_inadequate("1.3", req) + assert_adequate( "1.4", req) + assert_inadequate("1.5", req) + assert_adequate( "1.6", req) + assert_inadequate("1.7", req) + assert_inadequate("2.0", req) + end + + def test_spaceship + assert_equal 1, Gem::Version.new('1.8.2') <=> Gem::Version.new('0.0.0') + end + + def test_boxed + assert_inadequate("1.3", "~> 1.4") + assert_adequate( "1.4", "~> 1.4") + assert_adequate( "1.5", "~> 1.4") + assert_inadequate("2.0", "~> 1.4") + + assert_inadequate("1.3", "~> 1.4.4") + assert_inadequate("1.4", "~> 1.4.4") + assert_adequate( "1.4.4", "~> 1.4.4") + assert_adequate( "1.4.5", "~> 1.4.4") + assert_inadequate("1.5", "~> 1.4.4") + assert_inadequate("2.0", "~> 1.4.4") + end + + def test_to_s + v = Gem::Version.new("5.2.4") + assert_equal "5.2.4", v.to_s + end + + def assert_adequate(version, requirement) + ver = Gem::Version.new(version) + req = Gem::Version::Requirement.new(requirement) + assert req.satisfied_by?(ver), + "Version #{version} should be adequate for Requirement #{requirement}" + end + + def assert_inadequate(version, requirement) + ver = Gem::Version.new(version) + req = Gem::Version::Requirement.new(requirement) + assert ! req.satisfied_by?(ver), + "Version #{version} should not be adequate for Requirement #{requirement}" + end + + def assert_version(actual) + assert_equal @v1_0, actual + assert_equal @v1_0.version, actual.version + end + +end + diff --git a/test/rubygems/test_gem_version_option.rb b/test/rubygems/test_gem_version_option.rb new file mode 100644 index 0000000000..30c73390fe --- /dev/null +++ b/test/rubygems/test_gem_version_option.rb @@ -0,0 +1,77 @@ +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') +require 'rubygems/command' +require 'rubygems/version_option' + +class TestGemVersionOption < RubyGemTestCase + + def setup + super + + @cmd = Gem::Command.new 'dummy', 'dummy' + @cmd.extend Gem::VersionOption + end + + def test_add_platform_option + @cmd.add_platform_option + + assert @cmd.handles?(%w[--platform x86-darwin]) + end + + def test_add_version_option + @cmd.add_version_option + + assert @cmd.handles?(%w[--version >1]) + end + + def test_platform_option + @cmd.add_platform_option + + @cmd.handle_options %w[--platform x86-freebsd6 --platform x86-freebsd7] + + expected = [ + Gem::Platform::RUBY, + Gem::Platform.new('x86-freebsd6'), + Gem::Platform.new('x86-freebsd7'), + ] + + assert_equal expected, Gem.platforms + end + + def test_platform_option_ruby + @cmd.add_platform_option + + @cmd.handle_options %w[--platform ruby] + + expected = [ + Gem::Platform::RUBY + ] + + assert_equal expected, Gem.platforms + end + + def test_platform_option_twice + @cmd.add_platform_option + + @cmd.handle_options %w[--platform x86-freebsd6 --platform x86-freebsd-6] + + expected = [ + Gem::Platform::RUBY, + Gem::Platform.new('x86-freebsd6'), + ] + + assert_equal expected, Gem.platforms + end + + def test_version_option + @cmd.add_version_option + + @cmd.handle_options %w[--version >1] + + expected = { :version => Gem::Requirement.new('> 1'), :args => [] } + + assert_equal expected, @cmd.options + end + +end + diff --git a/test/rubygems/test_kernel.rb b/test/rubygems/test_kernel.rb new file mode 100644 index 0000000000..d7d5eaaee2 --- /dev/null +++ b/test/rubygems/test_kernel.rb @@ -0,0 +1,64 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require File.join(File.expand_path(File.dirname(__FILE__)), 'gemutilities') + +class TestKernel < RubyGemTestCase + + def setup + super + + @old_path = $:.dup + + util_make_gems + end + + def teardown + super + + $:.replace @old_path + end + + def test_gem + assert gem('a', '= 0.0.1'), "Should load" + assert $:.any? { |p| %r{a-0.0.1/lib} =~ p } + assert $:.any? { |p| %r{a-0.0.1/bin} =~ p } + end + + def test_gem_redundent + assert gem('a', '= 0.0.1'), "Should load" + assert ! gem('a', '= 0.0.1'), "Should not load" + assert_equal 1, $:.select { |p| %r{a-0.0.1/lib} =~ p }.size + assert_equal 1, $:.select { |p| %r{a-0.0.1/bin} =~ p }.size + end + + def test_gem_overlapping + assert gem('a', '= 0.0.1'), "Should load" + assert ! gem('a', '>= 0.0.1'), "Should not load" + assert_equal 1, $:.select { |p| %r{a-0.0.1/lib} =~ p }.size + assert_equal 1, $:.select { |p| %r{a-0.0.1/bin} =~ p }.size + end + + def test_gem_conflicting + assert gem('a', '= 0.0.1'), "Should load" + + ex = assert_raise Gem::Exception do + gem 'a', '= 0.0.2' + end + + assert_match(/activate a \(= 0\.0\.2\)/, ex.message) + assert_match(/activated a-0\.0\.1/, ex.message) + + assert $:.any? { |p| %r{a-0.0.1/lib} =~ p } + assert $:.any? { |p| %r{a-0.0.1/bin} =~ p } + assert ! $:.any? { |p| %r{a-0.0.2/lib} =~ p } + assert ! $:.any? { |p| %r{a-0.0.2/bin} =~ p } + end + +end + diff --git a/test/rubygems/test_open_uri.rb b/test/rubygems/test_open_uri.rb new file mode 100644 index 0000000000..6f18b7bb6b --- /dev/null +++ b/test/rubygems/test_open_uri.rb @@ -0,0 +1,13 @@ +require 'test/unit' +require 'rubygems/gem_open_uri' + +class TestOpenURI < Test::Unit::TestCase + + def test_open_uri_not_broken + assert_nothing_raised do + open __FILE__ do end + end + end + +end + diff --git a/test/rubygems/test_package.rb b/test/rubygems/test_package.rb new file mode 100644 index 0000000000..786400fe0c --- /dev/null +++ b/test/rubygems/test_package.rb @@ -0,0 +1,607 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'test/unit' +require 'stringio' +require 'fileutils' + +require 'rubygems' +require 'rubygems/package' + +class File + + # straight from setup.rb + def self.dir?(path) + # for corrupted windows stat() + File.directory?((path[-1,1] == '/') ? path : path + '/') + end + + def self.read_b(name) + File.open(name, "rb"){|f| f.read} + end + +end + +class TarTestCase < Test::Unit::TestCase + + undef_method :default_test + + def assert_headers_equal(h1, h2) + fields = %w[name 100 mode 8 uid 8 gid 8 size 12 mtime 12 checksum 8 + typeflag 1 linkname 100 magic 6 version 2 uname 32 + gname 32 devmajor 8 devminor 8 prefix 155] + offset = 0 + until fields.empty? + name = fields.shift + length = fields.shift.to_i + if name == "checksum" + chksum_off = offset + offset += length + next + end + assert_equal(h1[offset, length], h2[offset, length], + "Field #{name} of the tar header differs.") + offset += length + end + assert_equal(h1[chksum_off, 8], h2[chksum_off, 8]) + end + + def tar_file_header(fname, dname, mode, length) + h = header("0", fname, dname, length, mode) + checksum = calc_checksum(h) + header("0", fname, dname, length, mode, checksum) + end + + def tar_dir_header(name, prefix, mode) + h = header("5", name, prefix, 0, mode) + checksum = calc_checksum(h) + header("5", name, prefix, 0, mode, checksum) + end + + def header(type, fname, dname, length, mode, checksum = nil) + checksum ||= " " * 8 + + arr = [ # struct tarfile_entry_posix + ASCIIZ(fname, 100), # char name[100]; ASCII + (Z unless filled) + Z(to_oct(mode, 7)), # char mode[8]; 0 padded, octal null + 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 + 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) + "ustar\0", # char magic[6]; "ustar\0" + "00", # char version[2]; "00" + ASCIIZ("wheel", 32), # char uname[32]; ASCIIZ + ASCIIZ("wheel", 32), # char gname[32]; ASCIIZ + Z(to_oct(0, 7)), # char devmajor[8]; 0 padded, octal, null + Z(to_oct(0, 7)), # char devminor[8]; 0 padded, octal, null + ASCIIZ(dname, 155) # char prefix[155]; ASCII + (Z unless filled) + ] + + format = "C100C8C8C8C12C12C8CC100C6C2C32C32C8C8C155" + h = if RUBY_VERSION >= "1.9" then + arr.join + else + arr = arr.join("").split(//).map{|x| x[0]} + arr.pack format + end + ret = h + "\0" * (512 - h.size) + assert_equal(512, ret.size) + ret + end + + def calc_checksum(header) + sum = header.unpack("C*").inject{|s,a| s + a} + SP(Z(to_oct(sum, 6))) + end + + def to_oct(n, pad_size) + "%0#{pad_size}o" % n + end + + def ASCIIZ(str, length) + str + "\0" * (length - str.length) + end + + def SP(s) + s + " " + end + + def Z(s) + s + "\0" + end + + def SP_Z(s) + s + " \0" + end + +end + +class TestTarHeader < TarTestCase + + def test_arguments_are_checked + e = ArgumentError + gpth = Gem::Package::TarHeader + assert_raises(e) { gpth.new :name=>"", :size=>"", :mode=>"" } + assert_raises(e) { gpth.new :name=>"", :size=>"", :prefix=>"" } + assert_raises(e) { gpth.new :name=>"", :prefix=>"", :mode=>"" } + assert_raises(e) { gpth.new :prefix=>"", :size=>"", :mode=>"" } + end + + def test_basic_headers + header = Gem::Package::TarHeader.new(:name => "bla", :mode => 012345, + :size => 10, :prefix => "").to_s + assert_headers_equal(tar_file_header("bla", "", 012345, 10), header.to_s) + header = Gem::Package::TarHeader.new(:name => "bla", :mode => 012345, + :size => 0, :prefix => "", + :typeflag => "5" ).to_s + assert_headers_equal(tar_dir_header("bla", "", 012345), header) + end + + def test_long_name_works + header = Gem::Package::TarHeader.new(:name => "a" * 100, :mode => 012345, + :size => 10, :prefix => "").to_s + assert_headers_equal(tar_file_header("a" * 100, "", 012345, 10), header) + + header = Gem::Package::TarHeader.new(:name => "a" * 100, :mode => 012345, + :size => 10, :prefix => "bb" * 60).to_s + assert_headers_equal(tar_file_header("a" * 100, "bb" * 60, 012345, 10), + header) + end + + def test_new_from_stream + header = tar_file_header("a" * 100, "", 012345, 10) + h = nil + header = StringIO.new header + assert_nothing_raised{ h = Gem::Package::TarHeader.new_from_stream header } + assert_equal("a" * 100, h.name) + assert_equal(012345, h.mode) + assert_equal(10, h.size) + assert_equal("", h.prefix) + assert_equal("ustar", h.magic) + end + +end + +class TestTarInput < TarTestCase + + # Sometimes the setgid bit doesn't take. Don't know if this + # is a problem on all systems, or just some. But for now, we + # will ignore it in the tests. + SETGID_BIT = 02000 + + def setup + FileUtils.mkdir_p "data__" + inner_tar = tar_file_header("bla", "", 0612, 10) + inner_tar += "0123456789" + "\0" * 502 + inner_tar += tar_file_header("foo", "", 0636, 5) + inner_tar += "01234" + "\0" * 507 + inner_tar += tar_dir_header("__dir__", "", 0600) + inner_tar += "\0" * 1024 + str = StringIO.new "" + begin + os = Zlib::GzipWriter.new str + os.write inner_tar + ensure + os.finish + end + str.rewind + File.open("data__/bla.tar", "wb") do |f| + f.write tar_file_header("data.tar.gz", "", 0644, str.string.size) + f.write str.string + f.write "\0" * ((512 - (str.string.size % 512)) % 512 ) + @spec = Gem::Specification.new do |spec| + spec.author = "Mauricio :)" + end + meta = @spec.to_yaml + f.write tar_file_header("metadata", "", 0644, meta.size) + f.write meta + "\0" * (1024 - meta.size) + f.write "\0" * 1024 + end + @file = "data__/bla.tar" + @entry_names = %w{bla foo __dir__} + @entry_sizes = [10, 5, 0] + #FIXME: are these modes system dependent? + @entry_modes = [0100612, 0100636, 040600] + @entry_files = %w{data__/bla data__/foo} + @entry_contents = %w[0123456789 01234] + end + + def teardown + # FileUtils.rm_rf "data__" + end + + def test_each_works + Gem::Package::TarInput.open(@file) do |is| + count = 0 + + is.each_with_index do |entry, i| + count = i + + assert_kind_of(Gem::Package::TarReader::Entry, entry) + assert_equal(@entry_names[i], entry.name) + assert_equal(@entry_sizes[i], entry.size) + end + + assert_equal 2, count + + assert_equal @spec, is.metadata + end + end + + def test_extract_entry_works + Gem::Package::TarInput.open(@file) do |is| + assert_equal @spec, is.metadata + count = 0 + + is.each_with_index do |entry, i| + count = i + is.extract_entry "data__", entry + name = File.join("data__", entry.name) + + if entry.is_directory? + assert File.dir?(name) + else + assert File.file?(name) + assert_equal(@entry_sizes[i], File.stat(name).size) + #FIXME: win32? !! + end + + unless ::Config::CONFIG["arch"] =~ /msdos|win32/i + assert_equal(@entry_modes[i], + File.stat(name).mode & (~SETGID_BIT)) + end + end + + assert_equal 2, count + end + + @entry_files.each_with_index do |x, i| + assert(File.file?(x)) + assert_equal(@entry_contents[i], File.read_b(x)) + end + end + +end + +class TestTarOutput < TarTestCase + + def setup + FileUtils.mkdir_p "data__", :verbose=>false + @file = "data__/bla2.tar" + end + + def teardown + FileUtils.rm_rf "data__" + end + + def test_file_looks_good + Gem::Package::TarOutput.open(@file) do |os| + os.metadata = "bla".to_yaml + end + f = File.open(@file, "rb") + Gem::Package::TarReader.new(f) do |is| + i = 0 + is.each do |entry| + case i + when 0 + assert_equal("data.tar.gz", entry.name) + when 1 + assert_equal("metadata.gz", entry.name) + gzis = Zlib::GzipReader.new entry + assert_equal("bla".to_yaml, gzis.read) + gzis.close + end + i += 1 + end + assert_equal(2, i) + end + ensure + f.close + end + +end + +class TestTarReader < TarTestCase + + def test_eof_works + str = tar_file_header("bar", "baz", 0644, 0) + Gem::Package::TarReader.new(StringIO.new(str)) do |is| + is.each_entry do |entry| + assert_kind_of(Gem::Package::TarReader::Entry, entry) + data = entry.read + assert_equal(nil, data) + assert_equal(nil, entry.read(10)) + assert_equal(nil, entry.read) + assert_equal(nil, entry.getc) + assert_equal(true, entry.eof?) + end + end + str = tar_dir_header("foo", "bar", 012345) + Gem::Package::TarReader.new(StringIO.new(str)) do |is| + is.each_entry do |entry| + assert_kind_of(Gem::Package::TarReader::Entry, entry) + data = entry.read + assert_equal(nil, data) + assert_equal(nil, entry.read(10)) + assert_equal(nil, entry.read) + assert_equal(nil, entry.getc) + assert_equal(true, entry.eof?) + end + end + str = tar_dir_header("foo", "bar", 012345) + str += tar_file_header("bar", "baz", 0644, 0) + str += tar_file_header("bar", "baz", 0644, 0) + Gem::Package::TarReader.new(StringIO.new(str)) do |is| + is.each_entry do |entry| + assert_kind_of(Gem::Package::TarReader::Entry, entry) + data = entry.read + assert_equal(nil, data) + assert_equal(nil, entry.read(10)) + assert_equal(nil, entry.read) + assert_equal(nil, entry.getc) + assert_equal(true, entry.eof?) + end + end + end + + def test_multiple_entries + str = tar_file_header("lib/foo", "", 010644, 10) + "\0" * 512 + str += tar_file_header("bar", "baz", 0644, 0) + str += tar_dir_header("foo", "bar", 012345) + str += "\0" * 1024 + names = %w[lib/foo bar foo] + prefixes = ["", "baz", "bar"] + modes = [010644, 0644, 012345] + sizes = [10, 0, 0] + isdir = [false, false, true] + isfile = [true, true, false] + Gem::Package::TarReader.new(StringIO.new(str)) do |is| + i = 0 + is.each_entry do |entry| + assert_kind_of(Gem::Package::TarReader::Entry, entry) + assert_equal(names[i], entry.name) + assert_equal(prefixes[i], entry.prefix) + assert_equal(sizes[i], entry.size) + assert_equal(modes[i], entry.mode) + assert_equal(isdir[i], entry.is_directory?) + assert_equal(isfile[i], entry.is_file?) + if prefixes[i] != "" + assert_equal(File.join(prefixes[i], names[i]), + entry.full_name) + else + assert_equal(names[i], entry.name) + end + i += 1 + end + assert_equal(names.size, i) + end + end + + def test_read_works + contents = ('a'..'z').inject(""){|s,x| s << x * 100} + str = tar_file_header("lib/foo", "", 010644, contents.size) + contents + str += "\0" * (512 - (str.size % 512)) + Gem::Package::TarReader.new(StringIO.new(str)) do |is| + is.each_entry do |entry| + assert_kind_of(Gem::Package::TarReader::Entry, entry) + data = entry.read(3000) # bigger than contents.size + assert_equal(contents, data) + assert_equal(true, entry.eof?) + end + end + Gem::Package::TarReader.new(StringIO.new(str)) do |is| + is.each_entry do |entry| + assert_kind_of(Gem::Package::TarReader::Entry, entry) + data = entry.read(100) + (entry.size - data.size).times {|i| data << entry.getc.chr } + assert_equal(contents, data) + assert_equal(nil, entry.read(10)) + assert_equal(true, entry.eof?) + end + end + Gem::Package::TarReader.new(StringIO.new(str)) do |is| + is.each_entry do |entry| + assert_kind_of(Gem::Package::TarReader::Entry, entry) + data = entry.read + assert_equal(contents, data) + assert_equal(nil, entry.read(10)) + assert_equal(nil, entry.read) + assert_equal(nil, entry.getc) + assert_equal(true, entry.eof?) + end + end + end + + def test_rewind_entry_works + content = ('a'..'z').to_a.join(" ") + str = tar_file_header("lib/foo", "", 010644, content.size) + content + + "\0" * (512 - content.size) + str << "\0" * 1024 + Gem::Package::TarReader.new(StringIO.new(str)) do |is| + is.each_entry do |entry| + 3.times do + entry.rewind + assert_equal(content, entry.read) + assert_equal(content.size, entry.pos) + end + end + end + end + + def test_rewind_works + content = ('a'..'z').to_a.join(" ") + str = tar_file_header("lib/foo", "", 010644, content.size) + content + + "\0" * (512 - content.size) + str << "\0" * 1024 + Gem::Package::TarReader.new(StringIO.new(str)) do |is| + 3.times do + is.rewind + i = 0 + is.each_entry do |entry| + assert_equal(content, entry.read) + i += 1 + end + assert_equal(1, i) + end + end + end + +end + +class TestTarWriter < TarTestCase + + class DummyIO + attr_reader :data + def initialize + @data = "" + end + def write(dat) + data << dat + dat.size + end + def reset + @data = "" + end + end + + def setup + @data = "a" * 10 + @dummyos = DummyIO.new + @os = Gem::Package::TarWriter.new(@dummyos) + end + + def teardown + @os.close + end + + def test_add_file + dummyos = StringIO.new + class << dummyos + def method_missing(meth, *a) + self.string.send(meth, *a) + end + end + os = Gem::Package::TarWriter.new dummyos + content1 = ('a'..'z').to_a.join("") # 26 + content2 = ('aa'..'zz').to_a.join("") # 1352 + Gem::Package::TarWriter.new(dummyos) do |os| + os.add_file("lib/foo/bar", 0644) {|f| f.write "a" * 10 } + os.add_file("lib/bar/baz", 0644) {|f| f.write content1 } + os.add_file("lib/bar/baz", 0644) {|f| f.write content2 } + os.add_file("lib/bar/baz", 0644) {|f| } + end + assert_headers_equal(tar_file_header("lib/foo/bar", "", 0644, 10), + dummyos[0,512]) + assert_equal("a" * 10 + "\0" * 502, dummyos[512,512]) + offset = 512 * 2 + [content1, content2, ""].each do |data| + assert_headers_equal(tar_file_header("lib/bar/baz", "", 0644, + data.size), + dummyos[offset,512]) + offset += 512 + until !data || data == "" + chunk = data[0,512] + data[0,512] = "" + assert_equal(chunk + "\0" * (512-chunk.size), + dummyos[offset,512]) + offset += 512 + end + end + assert_equal("\0" * 1024, dummyos[offset,1024]) + end + + def test_add_file_simple + @dummyos.reset + Gem::Package::TarWriter.new(@dummyos) do |os| + os.add_file_simple("lib/foo/bar", 0644, 10) {|f| f.write "a" * 10 } + os.add_file_simple("lib/bar/baz", 0644, 100) {|f| f.write "fillme"} + end + assert_headers_equal(tar_file_header("lib/foo/bar", "", 0644, 10), + @dummyos.data[0,512]) + assert_equal("a" * 10 + "\0" * 502, @dummyos.data[512,512]) + assert_headers_equal(tar_file_header("lib/bar/baz", "", 0644, 100), + @dummyos.data[512*2,512]) + assert_equal("fillme" + "\0" * 506, @dummyos.data[512*3,512]) + assert_equal("\0" * 512, @dummyos.data[512*4, 512]) + assert_equal("\0" * 512, @dummyos.data[512*5, 512]) + end + + def test_add_file_tests_seekability + assert_raises(Gem::Package::NonSeekableIO) do + @os.add_file("libdfdsfd", 0644) {|f| } + end + end + + def test_file_name_is_split_correctly + # test insane file lengths, and + # a{100}/b{155}, etc + @dummyos.reset + names = ["a" * 155 + '/' + "b" * 100, "a" * 151 + "/" + ("qwer/" * 19) + "bla" ] + o_names = ["b" * 100, "qwer/" * 19 + "bla"] + o_prefixes = ["a" * 155, "a" * 151] + names.each {|name| @os.add_file_simple(name, 0644, 10) { } } + o_names.each_with_index do |nam, i| + assert_headers_equal(tar_file_header(nam, o_prefixes[i], 0644, 10), + @dummyos.data[2*i*512,512]) + end + assert_raises(Gem::Package::TooLongFileName) do + @os.add_file_simple(File.join("a" * 152, "b" * 10, "a" * 92), 0644,10) {} + end + assert_raises(Gem::Package::TooLongFileName) do + @os.add_file_simple(File.join("a" * 162, "b" * 10), 0644,10) {} + end + assert_raises(Gem::Package::TooLongFileName) do + @os.add_file_simple(File.join("a" * 10, "b" * 110), 0644,10) {} + end + end + + def test_file_size_is_checked + @dummyos.reset + assert_raises(Gem::Package::TarWriter::FileOverflow) do + @os.add_file_simple("lib/foo/bar", 0644, 10) {|f| f.write "1" * 100} + end + assert_nothing_raised do + @os.add_file_simple("lib/foo/bar", 0644, 10) {|f| } + end + end + + def test_write_data + @dummyos.reset + @os.add_file_simple("lib/foo/bar", 0644, 10) { |f| f.write @data } + @os.flush + assert_equal(@data + ("\0" * (512-@data.size)), + @dummyos.data[512,512]) + end + + def test_write_header + @dummyos.reset + @os.add_file_simple("lib/foo/bar", 0644, 0) { |f| } + @os.flush + assert_headers_equal(tar_file_header("lib/foo/bar", "", 0644, 0), + @dummyos.data[0,512]) + @dummyos.reset + @os.mkdir("lib/foo", 0644) + assert_headers_equal(tar_dir_header("lib/foo", "", 0644), + @dummyos.data[0,512]) + @os.mkdir("lib/bar", 0644) + assert_headers_equal(tar_dir_header("lib/bar", "", 0644), + @dummyos.data[512*1,512]) + end + + def test_write_operations_fail_after_closed + @dummyos.reset + @os.add_file_simple("sadd", 0644, 20) { |f| } + @os.close + assert_raises(Gem::Package::ClosedIO) { @os.flush } + assert_raises(Gem::Package::ClosedIO) { @os.add_file("dfdsf", 0644){} } + assert_raises(Gem::Package::ClosedIO) { @os.mkdir "sdfdsf", 0644 } + end + +end + -- cgit v1.2.3