From 8cc45aae947d453acca029e13eb64f3f5f0bf942 Mon Sep 17 00:00:00 2001 From: drbrain Date: Mon, 31 Mar 2008 22:40:06 +0000 Subject: Import RubyGems 1.1.0 git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@15873 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- lib/rubygems/builder.rb | 21 +- lib/rubygems/command_manager.rb | 1 + lib/rubygems/commands/cleanup_command.rb | 136 +++-- lib/rubygems/commands/environment_command.rb | 20 +- lib/rubygems/commands/fetch_command.rb | 16 +- lib/rubygems/commands/install_command.rb | 12 +- lib/rubygems/commands/list_command.rb | 6 +- lib/rubygems/commands/mirror_command.rb | 2 +- lib/rubygems/commands/query_command.rb | 57 +- lib/rubygems/commands/sources_command.rb | 25 +- lib/rubygems/commands/specification_command.rb | 12 +- lib/rubygems/commands/uninstall_command.rb | 13 +- lib/rubygems/commands/unpack_command.rb | 18 +- lib/rubygems/commands/update_command.rb | 43 +- lib/rubygems/custom_require.rb | 2 +- lib/rubygems/defaults.rb | 9 +- lib/rubygems/dependency_installer.rb | 176 +++--- lib/rubygems/exceptions.rb | 19 +- lib/rubygems/format.rb | 26 +- lib/rubygems/indexer.rb | 79 ++- lib/rubygems/indexer/abstract_index_builder.rb | 12 +- lib/rubygems/indexer/latest_index_builder.rb | 35 ++ lib/rubygems/indexer/master_index_builder.rb | 17 +- lib/rubygems/indexer/quick_index_builder.rb | 8 +- lib/rubygems/install_update_options.rb | 6 + lib/rubygems/installer.rb | 14 +- lib/rubygems/package.rb | 793 +------------------------ lib/rubygems/package/f_sync_dir.rb | 24 + lib/rubygems/package/tar_header.rb | 245 ++++++++ lib/rubygems/package/tar_input.rb | 219 +++++++ lib/rubygems/package/tar_output.rb | 143 +++++ lib/rubygems/package/tar_reader.rb | 86 +++ lib/rubygems/package/tar_reader/entry.rb | 99 +++ lib/rubygems/package/tar_writer.rb | 180 ++++++ lib/rubygems/remote_fetcher.rb | 147 ++++- lib/rubygems/requirement.rb | 2 + lib/rubygems/rubygems_version.rb | 2 +- lib/rubygems/security.rb | 1 + lib/rubygems/server.rb | 189 +++--- lib/rubygems/source_index.rb | 739 ++++++++++++----------- lib/rubygems/source_info_cache.rb | 312 +++++++--- lib/rubygems/source_info_cache_entry.rb | 18 +- lib/rubygems/specification.rb | 2 +- lib/rubygems/uninstaller.rb | 60 +- lib/rubygems/user_interaction.rb | 12 +- 45 files changed, 2379 insertions(+), 1679 deletions(-) create mode 100644 lib/rubygems/indexer/latest_index_builder.rb create mode 100644 lib/rubygems/package/f_sync_dir.rb create mode 100644 lib/rubygems/package/tar_header.rb create mode 100644 lib/rubygems/package/tar_input.rb create mode 100644 lib/rubygems/package/tar_output.rb create mode 100644 lib/rubygems/package/tar_reader.rb create mode 100644 lib/rubygems/package/tar_reader/entry.rb create mode 100644 lib/rubygems/package/tar_writer.rb (limited to 'lib/rubygems') diff --git a/lib/rubygems/builder.rb b/lib/rubygems/builder.rb index f7f07e86bf..6fd8528f56 100644 --- a/lib/rubygems/builder.rb +++ b/lib/rubygems/builder.rb @@ -65,13 +65,20 @@ EOM 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} + open @spec.file_name, 'wb' do |gem_io| + Gem::Package.open gem_io, 'w', @signer do |pkg| + pkg.metadata = @spec.to_yaml + + @spec.files.each do |file| + next if File.directory? file + + stat = File.stat file + mode = stat.mode & 0777 + size = stat.size + + pkg.add_file_simple file, mode, size do |tar_io| + tar_io.write open(file, "rb") { |f| f.read } + end end end end diff --git a/lib/rubygems/command_manager.rb b/lib/rubygems/command_manager.rb index a80c821c5c..b8aa651f56 100644 --- a/lib/rubygems/command_manager.rb +++ b/lib/rubygems/command_manager.rb @@ -123,6 +123,7 @@ module Gem end private + def load_and_instantiate(command_name) command_name = command_name.to_s retried = false diff --git a/lib/rubygems/commands/cleanup_command.rb b/lib/rubygems/commands/cleanup_command.rb index f6deac9829..40dcb9db34 100644 --- a/lib/rubygems/commands/cleanup_command.rb +++ b/lib/rubygems/commands/cleanup_command.rb @@ -2,92 +2,90 @@ require 'rubygems/command' require 'rubygems/source_index' require 'rubygems/dependency_list' -module Gem - module Commands - class CleanupCommand < Command - def initialize - super( - 'cleanup', +class Gem::Commands::CleanupCommand < Gem::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 + :force => false, :test => false, :install_dir => Gem.dir - def arguments # :nodoc: - "GEMNAME name of gem to cleanup" - end + add_option('-d', '--dryrun', "") do |value, options| + options[:dryrun] = true + end + end - def defaults_str # :nodoc: - "--no-dryrun" - 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..." + primary_gems = {} - def usage # :nodoc: - "#{program_name} [GEMNAME ...]" + Gem.source_index.each do |name, spec| + if primary_gems[spec.name].nil? or + primary_gems[spec.name].version < spec.version then + primary_gems[spec.name] = spec end + end - def execute - say "Cleaning up installed gems..." - srcindex = Gem::SourceIndex.from_installed_gems - primary_gems = {} + gems_to_cleanup = [] - 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 + 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 + Gem.source_index.each do |name, spec| + gems_to_cleanup << 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 + } - gems_to_cleanup = gems_to_cleanup.select { |spec| - primary_gems[spec.name].version != spec.version - } + uninstall_command = Gem::CommandManager.instance['uninstall'] + deplist = Gem::DependencyList.new + gems_to_cleanup.uniq.each do |spec| deplist.add spec end - uninstall_command = Gem::CommandManager.instance['uninstall'] - deplist = DependencyList.new - gems_to_cleanup.uniq.each do |spec| deplist.add(spec) end + deps = deplist.strongly_connected_components.flatten.reverse - 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}" + deps.each do |spec| + if options[:dryrun] then + say "Dry Run Mode: Would uninstall #{spec.full_name}" + else + say "Attempting to uninstall #{spec.full_name}" - options[:args] = [spec.name] - options[:version] = "= #{spec.version}" - options[:executables] = true + options[:args] = [spec.name] + options[:version] = "= #{spec.version}" + options[:executables] = false - uninstall_command.merge_options(options) + uninstaller = Gem::Uninstaller.new spec.name, options - begin - uninstall_command.execute - rescue Gem::DependencyRemovalException => ex - say "Unable to uninstall #{spec.full_name} ... continuing with remaining gems" - end - end + begin + uninstaller.uninstall + rescue Gem::DependencyRemovalException, + Gem::GemNotInHomeException => e + say "Unable to uninstall #{spec.full_name}:" + say "\t#{e.class}: #{e.message}" end - - say "Clean Up Complete" end end - + + say "Clean Up Complete" end + end + diff --git a/lib/rubygems/commands/environment_command.rb b/lib/rubygems/commands/environment_command.rb index ab85361753..56b373cfbe 100644 --- a/lib/rubygems/commands/environment_command.rb +++ b/lib/rubygems/commands/environment_command.rb @@ -25,19 +25,18 @@ class Gem::Commands::EnvironmentCommand < Gem::Command def execute out = '' arg = options[:args][0] - if begins?("packageversion", arg) then + case arg + when /^packageversion/ then out << Gem::RubyGemsPackageVersion - elsif begins?("version", arg) then + when /^version/ then out << Gem::RubyGemsVersion - elsif begins?("gemdir", arg) then + when /^gemdir/, /^gemhome/, /^home/, /^GEM_HOME/ then out << Gem.dir - elsif begins?("gempath", arg) then - out << Gem.path.join("\n") - elsif begins?("remotesources", arg) then + when /^gempath/, /^path/, /^GEM_PATH/ then + out << Gem.path.join(File::PATH_SEPARATOR) + when /^remotesources/ then out << Gem.sources.join("\n") - elsif arg then - fail Gem::CommandLineError, "Unknown enviroment option [#{arg}]" - else + when nil then out = "RubyGems Environment:\n" out << " - RUBYGEMS VERSION: #{Gem::RubyGemsVersion} (#{Gem::RubyGemsPackageVersion})\n" @@ -75,6 +74,9 @@ class Gem::Commands::EnvironmentCommand < Gem::Command Gem.sources.each do |s| out << " - #{s}\n" end + + else + fail Gem::CommandLineError, "Unknown enviroment option [#{arg}]" end say out true diff --git a/lib/rubygems/commands/fetch_command.rb b/lib/rubygems/commands/fetch_command.rb index 7db365eba0..ccedc45401 100644 --- a/lib/rubygems/commands/fetch_command.rb +++ b/lib/rubygems/commands/fetch_command.rb @@ -44,17 +44,15 @@ class Gem::Commands::FetchCommand < Gem::Command 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 + if spec.nil? then + alert_error "Could not find #{gem_name} in any repository" + next end - say "Downloaded #{gem_file}" + path = Gem::RemoteFetcher.fetcher.download spec, source_uri + FileUtils.mv path, "#{spec.full_name}.gem" + + say "Downloaded #{spec.full_name}" end end diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb index aa9f480c2a..ce0bc6ba04 100644 --- a/lib/rubygems/commands/install_command.rb +++ b/lib/rubygems/commands/install_command.rb @@ -62,13 +62,15 @@ class Gem::Commands::InstallCommand < Gem::Command :install_dir => options[:install_dir], :security_policy => options[:security_policy], :wrappers => options[:wrappers], + :bin_dir => options[:bin_dir] } + exit_code = 0 + get_all_gem_names.each do |gem_name| begin - inst = Gem::DependencyInstaller.new gem_name, options[:version], - install_options - inst.install + inst = Gem::DependencyInstaller.new install_options + inst.install gem_name, options[:version] inst.installed_gems.each do |spec| say "Successfully installed #{spec.full_name}" @@ -77,8 +79,10 @@ class Gem::Commands::InstallCommand < Gem::Command installed_gems.push(*inst.installed_gems) rescue Gem::InstallError => e alert_error "Error installing #{gem_name}:\n\t#{e.message}" + exit_code |= 1 rescue Gem::GemNotFoundException => e alert_error e.message + exit_code |= 2 # rescue => e # # TODO: Fix this handle to allow the error to propagate to # # the top level handler. Examine the other errors as @@ -121,6 +125,8 @@ class Gem::Commands::InstallCommand < Gem::Command end end end + + raise Gem::SystemExitException, exit_code end end diff --git a/lib/rubygems/commands/list_command.rb b/lib/rubygems/commands/list_command.rb index e179ff57ee..f8b377fcde 100644 --- a/lib/rubygems/commands/list_command.rb +++ b/lib/rubygems/commands/list_command.rb @@ -6,10 +6,8 @@ module Gem class ListCommand < QueryCommand def initialize - super( - 'list', - 'Display all gems whose name starts with STRING' - ) + super 'list', 'Display gems whose name starts with STRING' + remove_option('--name-matches') end diff --git a/lib/rubygems/commands/mirror_command.rb b/lib/rubygems/commands/mirror_command.rb index fc4f086ad3..959b8eaec3 100644 --- a/lib/rubygems/commands/mirror_command.rb +++ b/lib/rubygems/commands/mirror_command.rb @@ -2,7 +2,7 @@ require 'yaml' require 'zlib' require 'rubygems/command' -require 'rubygems/gem_open_uri' +require 'open-uri' class Gem::Commands::MirrorCommand < Gem::Command diff --git a/lib/rubygems/commands/query_command.rb b/lib/rubygems/commands/query_command.rb index 4f957625ee..fdc5a6a4ea 100644 --- a/lib/rubygems/commands/query_command.rb +++ b/lib/rubygems/commands/query_command.rb @@ -1,15 +1,25 @@ require 'rubygems/command' require 'rubygems/local_remote_options' require 'rubygems/source_info_cache' +require 'rubygems/version_option' class Gem::Commands::QueryCommand < Gem::Command include Gem::LocalRemoteOptions + include Gem::VersionOption def initialize(name = 'query', summary = 'Query gem information in local or remote repositories') super name, summary, - :name => /.*/, :domain => :local, :details => false, :versions => true + :name => //, :domain => :local, :details => false, :versions => true, + :installed => false, :version => Gem::Requirement.default + + add_option('-i', '--[no-]installed', + 'Check for installed gem') do |value, options| + options[:installed] = value + end + + add_version_option add_option('-n', '--name-matches REGEXP', 'Name of gem(s) to query on matches the', @@ -28,33 +38,70 @@ class Gem::Commands::QueryCommand < Gem::Command options[:details] = false unless value end + add_option('-a', '--all', + 'Display all gem versions') do |value, options| + options[:all] = value + end + add_local_remote_options end def defaults_str # :nodoc: - "--local --name-matches '.*' --no-details --versions" + "--local --name-matches // --no-details --versions --no-installed" end def execute + exit_code = 0 + name = options[:name] + if options[:installed] then + if name.source.empty? then + alert_error "You must specify a gem name" + exit_code |= 4 + elsif installed? name.source, options[:version] then + say "true" + else + say "false" + exit_code |= 1 + end + + raise Gem::SystemExitException, exit_code + end + if local? then say say "*** LOCAL GEMS ***" say - output_query_results Gem.cache.search(name) + + output_query_results Gem.source_index.search(name) end if remote? then say say "*** REMOTE GEMS ***" say - output_query_results Gem::SourceInfoCache.search(name) + + begin + Gem::SourceInfoCache.cache.refresh options[:all] + rescue Gem::RemoteFetcher::FetchError + # no network + end + + output_query_results Gem::SourceInfoCache.search(name, false, true) end end private + ## + # Check if gem +name+ version +version+ is installed. + + def installed?(name, version = Gem::Requirement.default) + dep = Gem::Dependency.new name, version + !Gem.source_index.search(dep).empty? + end + def output_query_results(gemspecs) output = [] gem_list_with_version = {} @@ -98,7 +145,7 @@ class Gem::Commands::QueryCommand < Gem::Command ## # Used for wrapping and indenting text - # + def format_text(text, wrap, indent=0) result = [] work = text.dup diff --git a/lib/rubygems/commands/sources_command.rb b/lib/rubygems/commands/sources_command.rb index a0977f90dc..6d9d5b5b90 100644 --- a/lib/rubygems/commands/sources_command.rb +++ b/lib/rubygems/commands/sources_command.rb @@ -39,8 +39,11 @@ class Gem::Commands::SourcesCommand < Gem::Command 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) + sic = Gem::SourceInfoCache + remove_cache_file 'user', sic.user_cache_file + remove_cache_file 'latest user', sic.latest_user_cache_file + remove_cache_file 'system', sic.system_cache_file + remove_cache_file 'latest system', sic.latest_system_cache_file end if options[:add] then @@ -48,7 +51,7 @@ class Gem::Commands::SourcesCommand < Gem::Command sice = Gem::SourceInfoCacheEntry.new nil, nil begin - sice.refresh source_uri + sice.refresh source_uri, true Gem::SourceInfoCache.cache_data[source_uri] = sice Gem::SourceInfoCache.cache.update @@ -66,7 +69,7 @@ class Gem::Commands::SourcesCommand < Gem::Command end if options[:update] then - Gem::SourceInfoCache.cache.refresh + Gem::SourceInfoCache.cache.refresh true Gem::SourceInfoCache.cache.flush say "source cache successfully updated" @@ -78,6 +81,11 @@ class Gem::Commands::SourcesCommand < Gem::Command unless Gem.sources.include? source_uri then say "source #{source_uri} not present in cache" else + begin # HACK figure out how to get the cache w/o update + Gem::SourceInfoCache.cache + rescue Gem::RemoteFetcher::FetchError + end + Gem::SourceInfoCache.cache_data.delete source_uri Gem::SourceInfoCache.cache.update Gem::SourceInfoCache.cache.flush @@ -100,11 +108,12 @@ class Gem::Commands::SourcesCommand < Gem::Command private - def remove_cache_file(desc, fn) - FileUtils.rm_rf fn rescue nil - if ! File.exist?(fn) + def remove_cache_file(desc, path) + FileUtils.rm_rf path + + if not File.exist?(path) then say "*** Removed #{desc} source cache ***" - elsif ! File.writable?(fn) + elsif not File.writable?(path) then say "*** Unable to remove #{desc} source cache (write protected) ***" else say "*** Unable to remove #{desc} source cache ***" diff --git a/lib/rubygems/commands/specification_command.rb b/lib/rubygems/commands/specification_command.rb index 1ab2ad9260..7c8598e53b 100644 --- a/lib/rubygems/commands/specification_command.rb +++ b/lib/rubygems/commands/specification_command.rb @@ -3,6 +3,7 @@ require 'rubygems/command' require 'rubygems/local_remote_options' require 'rubygems/version_option' require 'rubygems/source_info_cache' +require 'rubygems/format' class Gem::Commands::SpecificationCommand < Gem::Command @@ -41,13 +42,16 @@ class Gem::Commands::SpecificationCommand < Gem::Command 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])) + if File.exist? gem then + specs << Gem::Format.from_file_by_path(gem).spec rescue nil + end + + if specs.empty? then + specs.push(*Gem.source_index.search(/\A#{gem}\z/, options[:version])) + end 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 diff --git a/lib/rubygems/commands/uninstall_command.rb b/lib/rubygems/commands/uninstall_command.rb index 2d9c46ee52..3d6e2383bc 100644 --- a/lib/rubygems/commands/uninstall_command.rb +++ b/lib/rubygems/commands/uninstall_command.rb @@ -35,6 +35,11 @@ module Gem options[:install_dir] = File.expand_path(value) end + add_option('-n', '--bindir DIR', + 'Directory to remove binaries from') do |value, options| + options[:bin_dir] = File.expand_path(value) + end + add_version_option add_platform_option end @@ -54,7 +59,13 @@ module Gem def execute get_all_gem_names.each do |gem_name| - Gem::Uninstaller.new(gem_name, options).uninstall + begin + Gem::Uninstaller.new(gem_name, options).uninstall + rescue Gem::GemNotInHomeException => e + spec = e.spec + alert("In order to remove #{spec.name}, please execute:\n" \ + "\tgem uninstall #{spec.name} --install-dir=#{spec.installation_path}") + end end end end diff --git a/lib/rubygems/commands/unpack_command.rb b/lib/rubygems/commands/unpack_command.rb index 23ebabc21a..d187f8a9ea 100644 --- a/lib/rubygems/commands/unpack_command.rb +++ b/lib/rubygems/commands/unpack_command.rb @@ -38,6 +38,7 @@ class Gem::Commands::UnpackCommand < Gem::Command def execute gemname = get_one_gem_name path = get_path(gemname, options[:version]) + if path then basename = File.basename(path).sub(/\.gem$/, '') target_dir = File.expand_path File.join(options[:target], basename) @@ -66,16 +67,27 @@ class Gem::Commands::UnpackCommand < Gem::Command # 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) + + specs = Gem::source_index.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) + path = nil + + Gem.path.find do |gem_dir| + path = File.join gem_dir, 'cache', filename + File.exist? path + end + + path else - return nil + nil end end diff --git a/lib/rubygems/commands/update_command.rb b/lib/rubygems/commands/update_command.rb index 88d48d705e..b8de911e20 100644 --- a/lib/rubygems/commands/update_command.rb +++ b/lib/rubygems/commands/update_command.rb @@ -1,8 +1,10 @@ require 'rubygems/command' +require 'rubygems/command_manager' require 'rubygems/install_update_options' require 'rubygems/local_remote_options' require 'rubygems/source_info_cache' require 'rubygems/version_option' +require 'rubygems/commands/install_command' class Gem::Commands::UpdateCommand < Gem::Command @@ -45,7 +47,7 @@ class Gem::Commands::UpdateCommand < Gem::Command def execute if options[:system] then - say "Updating RubyGems..." + say "Updating RubyGems" unless options[:args].empty? then fail "No gem names are allowed with the --system option" @@ -53,10 +55,10 @@ class Gem::Commands::UpdateCommand < Gem::Command options[:args] = ["rubygems-update"] else - say "Updating installed gems..." + say "Updating installed gems" end - hig = highest_installed_gems = {} + hig = {} Gem::SourceIndex.from_installed_gems.each do |name, spec| if hig[spec.name].nil? or hig[spec.name].version < spec.version then @@ -64,25 +66,28 @@ class Gem::Commands::UpdateCommand < Gem::Command end end - remote_gemspecs = Gem::SourceInfoCache.search(//) + pattern = if options[:args].empty? then + // + else + Regexp.union(*options[:args]) + end - gems_to_update = if options[:args].empty? then - which_to_update(highest_installed_gems, remote_gemspecs) - else - options[:args] - end + remote_gemspecs = Gem::SourceInfoCache.search pattern - options[:domain] = :remote # install from remote source + gems_to_update = which_to_update hig, remote_gemspecs - # HACK use the real API - install_command = Gem::CommandManager.instance['install'] + updated = [] + # HACK use the real API 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 + next if updated.any? { |spec| spec.name == name } + say "Updating #{name}" + installer = Gem::DependencyInstaller.new options + installer.install name + installer.installed_gems.each do |spec| + updated << spec + say "Successfully installed #{spec.full_name}" + end end if gems_to_update.include? "rubygems-update" then @@ -97,12 +102,10 @@ class Gem::Commands::UpdateCommand < Gem::Command say "RubyGems system software updated" if installed else - updated = gems_to_update.uniq.sort.collect { |g| g.to_s } - if updated.empty? then say "Nothing to update" else - say "Gems updated: #{updated.join ', '}" + say "Gems updated: #{updated.map { |spec| spec.name }.join ', '}" end end end diff --git a/lib/rubygems/custom_require.rb b/lib/rubygems/custom_require.rb index 598ec3ef98..5ff65afb14 100755 --- a/lib/rubygems/custom_require.rb +++ b/lib/rubygems/custom_require.rb @@ -28,7 +28,7 @@ module Kernel 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.activate(spec.name, "= #{spec.version}") gem_original_require path else raise load_error diff --git a/lib/rubygems/defaults.rb b/lib/rubygems/defaults.rb index 3a6229511b..3864e5faca 100644 --- a/lib/rubygems/defaults.rb +++ b/lib/rubygems/defaults.rb @@ -11,6 +11,9 @@ module Gem if defined? RUBY_FRAMEWORK_VERSION then File.join File.dirname(ConfigMap[:sitedir]), 'Gems', ConfigMap[:ruby_version] + elsif defined? RUBY_ENGINE then + File.join ConfigMap[:libdir], RUBY_ENGINE, 'gems', + ConfigMap[:ruby_version] else File.join ConfigMap[:libdir], 'ruby', 'gems', ConfigMap[:ruby_version] end @@ -29,7 +32,11 @@ module Gem # The default directory for binaries def self.default_bindir - Config::CONFIG['bindir'] + if defined? RUBY_FRAMEWORK_VERSION then # mac framework support + '/usr/bin' + else # generic install + ConfigMap[:bindir] + end end # The default system-wide source info cache directory. diff --git a/lib/rubygems/dependency_installer.rb b/lib/rubygems/dependency_installer.rb index ec8a50d912..26ef41b2f1 100644 --- a/lib/rubygems/dependency_installer.rb +++ b/lib/rubygems/dependency_installer.rb @@ -22,8 +22,7 @@ class Gem::DependencyInstaller } ## - # Creates a new installer instance that will install +gem_name+ using - # version requirement +version+ and +options+. + # Creates a new installer instance. # # Options are: # :env_shebang:: See Gem::Installer::new. @@ -36,7 +35,7 @@ class Gem::DependencyInstaller # :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 = {}) + def initialize(options = {}) options = DEFAULT_OPTIONS.merge options @env_shebang = options[:env_shebang] @domain = options[:domain] @@ -46,49 +45,9 @@ class Gem::DependencyInstaller @install_dir = options[:install_dir] || Gem.dir @security_policy = options[:security_policy] @wrappers = options[:wrappers] + @bin_dir = options[:bin_dir] @installed_gems = [] - - spec_and_source = nil - - glob = if File::ALT_SEPARATOR then - gem_name.gsub File::ALT_SEPARATOR, File::SEPARATOR - else - gem_name - end - - local_gems = Dir["#{glob}*"].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 ## @@ -107,71 +66,30 @@ class Gem::DependencyInstaller 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 - begin - 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 - rescue Gem::RemoteFetcher::FetchError - raise if spec.original_platform == spec.platform - - alternate_name = "#{spec.name}-#{spec.version}-#{spec.original_platform}.gem" + begin + requirements = dep.version_requirements.requirements.map do |req, ver| + req + end - say "Failed, downloading gem #{alternate_name}" if - Gem.configuration.really_verbose + all = requirements.length > 1 || + requirements.first != ">=" || requirements.first != ">" - remote_gem_path = source_uri + "gems/#{alternate_name}" + found = Gem::SourceInfoCache.search_with_source dep, true, all - gem = Gem::RemoteFetcher.fetcher.fetch_path remote_gem_path - end + gems_and_sources.push(*found) - File.open local_gem_path, 'wb' do |fp| - fp.write gem + rescue Gem::RemoteFetcher::FetchError => e + if Gem.configuration.really_verbose then + say "Error fetching remote data:\t\t#{e.message}" + say "Falling back to local-only install" end + @domain = :local end - 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 + gems_and_sources.sort_by do |gem, source| + [gem, source =~ /^http:\/\// ? 0 : 1] # local gems win + end end ## @@ -208,9 +126,57 @@ class Gem::DependencyInstaller @gems_to_install = dependency_list.dependency_order.reverse end + def find_spec_by_name_and_version gem_name, version = Gem::Requirement.default + spec_and_source = nil + + glob = if File::ALT_SEPARATOR then + gem_name.gsub File::ALT_SEPARATOR, File::SEPARATOR + else + gem_name + end + + local_gems = Dir["#{glob}*"].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 + dep = Gem::Dependency.new gem_name, version + spec_and_sources = find_gems_with_sources(dep).reverse + + spec_and_source = spec_and_sources.find { |spec, source| + Gem::Platform.match spec.platform + } + 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] + end + ## # Installs the gem and all its dependencies. - def install + def install dep_or_name, version = Gem::Requirement.default + if String === dep_or_name then + find_spec_by_name_and_version dep_or_name, version + else + @specs_and_sources = [find_gems_with_sources(dep_or_name).last] + end + + gather_dependencies + spec_dir = File.join @install_dir, 'specifications' source_index = Gem::SourceIndex.from_gems_in spec_dir @@ -219,10 +185,11 @@ class Gem::DependencyInstaller # HACK is this test for full_name acceptable? next if source_index.any? { |n,_| n == spec.full_name } and not last + # TODO: make this sorta_verbose so other users can benefit from it say "Installing gem #{spec.full_name}" if Gem.configuration.really_verbose _, source_uri = @specs_and_sources.assoc spec - local_gem_path = download spec, source_uri + local_gem_path = Gem::RemoteFetcher.fetcher.download spec, source_uri inst = Gem::Installer.new local_gem_path, :env_shebang => @env_shebang, @@ -231,7 +198,8 @@ class Gem::DependencyInstaller :ignore_dependencies => @ignore_dependencies, :install_dir => @install_dir, :security_policy => @security_policy, - :wrappers => @wrappers + :wrappers => @wrappers, + :bin_dir => @bin_dir spec = inst.install diff --git a/lib/rubygems/exceptions.rb b/lib/rubygems/exceptions.rb index b34bc718ff..c37507c62a 100644 --- a/lib/rubygems/exceptions.rb +++ b/lib/rubygems/exceptions.rb @@ -13,7 +13,10 @@ class Gem::DependencyRemovalException < Gem::Exception; end ## # Raised when attempting to uninstall a gem that isn't in GEM_HOME. -class Gem::GemNotInHomeException < Gem::Exception; end + +class Gem::GemNotInHomeException < Gem::Exception + attr_accessor :spec +end class Gem::DocumentError < Gem::Exception; end @@ -65,3 +68,17 @@ class Gem::RemoteSourceException < Gem::Exception; end class Gem::VerificationError < Gem::Exception; end +## +# Raised to indicate that a system exit should occur with the specified +# exit_code + +class Gem::SystemExitException < SystemExit + attr_accessor :exit_code + + def initialize(exit_code) + @exit_code = exit_code + + super "Exiting RubyGems with exit_code #{exit_code}" + end + +end diff --git a/lib/rubygems/format.rb b/lib/rubygems/format.rb index 378a93018c..7dc127d5f4 100644 --- a/lib/rubygems/format.rb +++ b/lib/rubygems/format.rb @@ -43,15 +43,12 @@ module Gem # 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? + open file_path, Gem.binary_mode do |io| + format = from_io io, file_path, security_policy end end @@ -65,15 +62,24 @@ module Gem # 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 = new gem_path + + Package.open 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] + size = entry.header.size + mode = entry.header.mode + + format.file_entries << [{ + "size" => size, "mode" => mode, "path" => entry.full_name, + }, + entry.read + ] end end + format end diff --git a/lib/rubygems/indexer.rb b/lib/rubygems/indexer.rb index 272cee3fd3..5496e452cc 100644 --- a/lib/rubygems/indexer.rb +++ b/lib/rubygems/indexer.rb @@ -11,6 +11,7 @@ end ## # Top level class for building the gem repository index. + class Gem::Indexer include Gem::UserInteraction @@ -25,7 +26,9 @@ class Gem::Indexer 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:" \ @@ -39,52 +42,60 @@ class Gem::Indexer @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 + @quick_index = Gem::Indexer::QuickIndexBuilder.new 'index', @directory + + quick_dir = File.join @directory, 'quick' + @latest_index = Gem::Indexer::LatestIndexBuilder.new 'latest_index', quick_dir 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, + @latest_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 - - unless gemfile =~ /\/#{Regexp.escape spec.original_name}.*\.gem\z/i then - alert_warning "Skipping misnamed gem: #{gemfile} => #{spec.full_name} (#{spec.original_name})" + gem_file_list.each do |gemfile| + if File.size(gemfile.to_s) == 0 then + alert_warning "Skipping zero-length gem: #{gemfile}" next end - abbreviate spec - sanitize spec + begin + spec = Gem::Format.from_file_by_path(gemfile).spec - @master_index.add spec - @quick_index.add spec - @marshal_index.add spec + unless gemfile =~ /\/#{Regexp.escape spec.original_name}.*\.gem\z/i then + alert_warning "Skipping misnamed gem: #{gemfile} => #{spec.full_name} (#{spec.original_name})" + next + end - progress.updated spec.original_name + abbreviate spec + sanitize spec - 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 + @master_index.add spec + @quick_index.add spec + @marshal_index.add spec + @latest_index.add spec + + progress.updated spec.original_name - progress.done + 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)" + say "Generating master indexes (this may take a while)" + end end end end @@ -95,14 +106,15 @@ class Gem::Indexer say "Moving index into production dir #{@dest_directory}" if verbose - files = @master_index.files + @quick_index.files + @marshal_index.files + files = @master_index.files + @quick_index.files + @marshal_index.files + + @latest_index.files files.each do |file| - relative_name = file[/\A#{Regexp.escape @directory}.(.*)/, 1] - dest_name = File.join @dest_directory, relative_name + src_name = File.join @directory, file + dst_name = File.join @dest_directory, file - FileUtils.rm_rf dest_name, :verbose => verbose - FileUtils.mv file, @dest_directory, :verbose => verbose + FileUtils.rm_rf dst_name, :verbose => verbose + FileUtils.mv src_name, @dest_directory, :verbose => verbose end end @@ -160,4 +172,5 @@ require 'rubygems/indexer/abstract_index_builder' require 'rubygems/indexer/master_index_builder' require 'rubygems/indexer/quick_index_builder' require 'rubygems/indexer/marshal_index_builder' +require 'rubygems/indexer/latest_index_builder' diff --git a/lib/rubygems/indexer/abstract_index_builder.rb b/lib/rubygems/indexer/abstract_index_builder.rb index f25f21707b..5815dcda87 100644 --- a/lib/rubygems/indexer/abstract_index_builder.rb +++ b/lib/rubygems/indexer/abstract_index_builder.rb @@ -22,16 +22,18 @@ class Gem::Indexer::AbstractIndexBuilder @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 + @files << @filename File.open file_path, "wb" do |file| @file = file @@ -39,14 +41,20 @@ class Gem::Indexer::AbstractIndexBuilder 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 }) + data = open filename, 'rb' do |fp| fp.read end + + zipped = zip data + File.open "#{filename}.#{ext}", "wb" do |file| file.write zipped end diff --git a/lib/rubygems/indexer/latest_index_builder.rb b/lib/rubygems/indexer/latest_index_builder.rb new file mode 100644 index 0000000000..a5798580a6 --- /dev/null +++ b/lib/rubygems/indexer/latest_index_builder.rb @@ -0,0 +1,35 @@ +require 'rubygems/indexer' + +## +# Construct the latest Gem index file. + +class Gem::Indexer::LatestIndexBuilder < Gem::Indexer::AbstractIndexBuilder + + def start_index + super + + @index = Gem::SourceIndex.new + end + + def end_index + super + + latest = @index.latest_specs.sort.map { |spec| spec.original_name } + + @file.write latest.join("\n") + end + + def cleanup + super + + compress @file.path + + @files.delete 'latest_index' # HACK installed via QuickIndexBuilder :/ + end + + def add(spec) + @index.add_spec(spec) + end + +end + diff --git a/lib/rubygems/indexer/master_index_builder.rb b/lib/rubygems/indexer/master_index_builder.rb index dbe02370a9..669ea5a1df 100644 --- a/lib/rubygems/indexer/master_index_builder.rb +++ b/lib/rubygems/indexer/master_index_builder.rb @@ -1,6 +1,8 @@ require 'rubygems/indexer' +## # Construct the master Gem index file. + class Gem::Indexer::MasterIndexBuilder < Gem::Indexer::AbstractIndexBuilder def start_index @@ -10,6 +12,7 @@ class Gem::Indexer::MasterIndexBuilder < Gem::Indexer::AbstractIndexBuilder def end_index super + @file.puts "--- !ruby/object:#{@index.class}" @file.puts "gems:" @@ -28,11 +31,9 @@ class Gem::Indexer::MasterIndexBuilder < Gem::Indexer::AbstractIndexBuilder 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 + paranoid index_file_name, "#{index_file_name}.Z" - @files << compressed_file_name + @files << "#{@filename}.Z" end def add(spec) @@ -41,12 +42,12 @@ class Gem::Indexer::MasterIndexBuilder < Gem::Indexer::AbstractIndexBuilder 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 + def paranoid(path, compressed_path) + data = Gem.read_binary path + compressed_data = Gem.read_binary compressed_path if data != unzip(compressed_data) then - fail "Compressed file #{compressed_fn} does not match uncompressed file #{fn}" + raise "Compressed file #{compressed_path} does not match uncompressed file #{path}" end end diff --git a/lib/rubygems/indexer/quick_index_builder.rb b/lib/rubygems/indexer/quick_index_builder.rb index 23c7ca696b..dc36179dc5 100644 --- a/lib/rubygems/indexer/quick_index_builder.rb +++ b/lib/rubygems/indexer/quick_index_builder.rb @@ -1,7 +1,9 @@ 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) @@ -13,12 +15,12 @@ class Gem::Indexer::QuickIndexBuilder < Gem::Indexer::AbstractIndexBuilder def cleanup super - quick_index_file = File.join(@directory, @filename) + 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 + @files.delete 'index' + @files << 'quick' end def add(spec) diff --git a/lib/rubygems/install_update_options.rb b/lib/rubygems/install_update_options.rb index af6be423f6..58807be62a 100644 --- a/lib/rubygems/install_update_options.rb +++ b/lib/rubygems/install_update_options.rb @@ -25,6 +25,12 @@ module Gem::InstallUpdateOptions options[:install_dir] = File.expand_path(value) end + add_option(:"Install/Update", '-n', '--bindir DIR', + 'Directory where binary files are', + 'located') do |value, options| + options[:bin_dir] = File.expand_path(value) + end + add_option(:"Install/Update", '-d', '--[no-]rdoc', 'Generate RDoc documentation for the gem on', 'install') do |value, options| diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 552a803c12..9dbbca8d08 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -63,7 +63,8 @@ class Gem::Installer :force => false, :install_dir => Gem.dir, :exec_format => false, - :env_shebang => false + :env_shebang => false, + :bin_dir => nil }.merge options @env_shebang = options[:env_shebang] @@ -74,6 +75,7 @@ class Gem::Installer @format_executable = options[:format_executable] @security_policy = options[:security_policy] @wrappers = options[:wrappers] + @bin_dir = options[:bin_dir] begin @format = Gem::Format.from_file_by_path @gem, @security_policy @@ -104,7 +106,7 @@ class Gem::Installer unless @force then if rrv = @spec.required_ruby_version then - unless rrv.satisfied_by? Gem::Version.new(RUBY_VERSION) then + unless rrv.satisfied_by? Gem.ruby_version then raise Gem::InstallError, "#{@spec.name} requires Ruby version #{rrv}" end end @@ -225,7 +227,7 @@ class Gem::Installer # 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 + bindir = @bin_dir ? @bin_dir : (Gem.bindir @gem_home) Dir.mkdir bindir unless File.exist? bindir raise Gem::FilePermissionError.new(bindir) unless File.writable? bindir @@ -303,7 +305,7 @@ class Gem::Installer # necessary. def shebang(bin_file_name) if @env_shebang then - "#!/usr/bin/env ruby" + "#!/usr/bin/env " + Gem::ConfigMap[:ruby_install_name] else path = File.join @gem_dir, @spec.bindir, bin_file_name @@ -352,10 +354,10 @@ TEXT <<-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 +@"#{File.basename(Gem.ruby)}" "#{File.join(bindir, bin_file_name)}" %1 %2 %3 %4 %5 %6 %7 %8 %9 GOTO :EOF :WinNT -"%~dp0ruby.exe" "%~dpn0" %* +@"#{File.basename(Gem.ruby)}" "%~dpn0" %* TEXT end diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index f15e4feecb..9cb393b0c7 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.rb @@ -45,768 +45,15 @@ module Gem::Package 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.split('').map { |c| c[0] }.inject { |a, b| a + b } # HACK rubinius - 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) - if defined? Rubinius then - zis = Zlib::GzipReader.new entry - dis = zis.read - is = StringIO.new(dis) - else - # This is Jamis Buck's ZLib workaround for some unknown issue - entry.read(10) # skip the gzip header - zis = Zlib::Inflate.new(-Zlib::MAX_WBITS) - is = StringIO.new(zis.inflate(entry.read)) - end - 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 + def self.open(io, mode = "r", signer = nil, &block) + tar_type = case mode + when 'r' then TarInput + when 'w' then TarOutput + else + raise "Unknown Package open mode" + end + + tar_type.open(io, signer, &block) end def self.pack(src, destname, signer = nil) @@ -836,19 +83,13 @@ module Gem::Package end end - class << self - def file_class - File - end - - def dir_class - Dir - end - - def find_class # HACK kill me - Find - end - end - end +require 'rubygems/package/f_sync_dir' +require 'rubygems/package/tar_header' +require 'rubygems/package/tar_input' +require 'rubygems/package/tar_output' +require 'rubygems/package/tar_reader' +require 'rubygems/package/tar_reader/entry' +require 'rubygems/package/tar_writer' + diff --git a/lib/rubygems/package/f_sync_dir.rb b/lib/rubygems/package/f_sync_dir.rb new file mode 100644 index 0000000000..3e2e4a59a8 --- /dev/null +++ b/lib/rubygems/package/f_sync_dir.rb @@ -0,0 +1,24 @@ +#++ +# Copyright (C) 2004 Mauricio Julio Fernández Pradier +# See LICENSE.txt for additional licensing information. +#-- + +require 'rubygems/package' + +module Gem::Package::FSyncDir + + private + + ## + # make sure this hits the disc + + def fsync_dir(dirname) + dir = open dirname, 'r' + dir.fsync + rescue # ignore IOError if it's an unpatched (old) Ruby + ensure + dir.close if dir rescue nil + end + +end + diff --git a/lib/rubygems/package/tar_header.rb b/lib/rubygems/package/tar_header.rb new file mode 100644 index 0000000000..c194cc0530 --- /dev/null +++ b/lib/rubygems/package/tar_header.rb @@ -0,0 +1,245 @@ +#++ +# Copyright (C) 2004 Mauricio Julio Fernández Pradier +# See LICENSE.txt for additional licensing information. +#-- + +require 'rubygems/package' + +## +#-- +# 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) +# }; +#++ + +class Gem::Package::TarHeader + + FIELDS = [ + :checksum, + :devmajor, + :devminor, + :gid, + :gname, + :linkname, + :magic, + :mode, + :mtime, + :name, + :prefix, + :size, + :typeflag, + :uid, + :uname, + :version, + ] + + PACK_FORMAT = 'a100' + # name + 'a8' + # mode + 'a8' + # uid + 'a8' + # gid + 'a12' + # size + 'a12' + # mtime + 'a7a' + # chksum + 'a' + # typeflag + 'a100' + # linkname + 'a6' + # magic + 'a2' + # version + 'a32' + # uname + 'a32' + # gname + 'a8' + # devmajor + 'a8' + # devminor + 'a155' # prefix + + UNPACK_FORMAT = 'A100' + # name + 'A8' + # mode + 'A8' + # uid + 'A8' + # gid + 'A12' + # size + 'A12' + # mtime + 'A8' + # checksum + 'A' + # typeflag + 'A100' + # linkname + 'A6' + # magic + 'A2' + # version + 'A32' + # uname + 'A32' + # gname + 'A8' + # devmajor + 'A8' + # devminor + 'A155' # prefix + + attr_reader(*FIELDS) + + def self.from(stream) + header = stream.read 512 + empty = (header == "\0" * 512) + + fields = header.unpack UNPACK_FORMAT + + name = fields.shift + mode = fields.shift.oct + uid = fields.shift.oct + gid = fields.shift.oct + size = fields.shift.oct + mtime = fields.shift.oct + checksum = fields.shift.oct + typeflag = fields.shift + linkname = fields.shift + magic = fields.shift + version = fields.shift.oct + uname = fields.shift + gname = fields.shift + devmajor = fields.shift.oct + devminor = fields.shift.oct + prefix = fields.shift + + new :name => name, + :mode => mode, + :uid => uid, + :gid => gid, + :size => size, + :mtime => mtime, + :checksum => checksum, + :typeflag => typeflag, + :linkname => linkname, + :magic => magic, + :version => version, + :uname => uname, + :gname => gname, + :devmajor => devmajor, + :devminor => devminor, + :prefix => prefix, + + :empty => empty + + # HACK unfactor for Rubinius + #new :name => fields.shift, + # :mode => fields.shift.oct, + # :uid => fields.shift.oct, + # :gid => fields.shift.oct, + # :size => fields.shift.oct, + # :mtime => fields.shift.oct, + # :checksum => fields.shift.oct, + # :typeflag => fields.shift, + # :linkname => fields.shift, + # :magic => fields.shift, + # :version => fields.shift.oct, + # :uname => fields.shift, + # :gname => fields.shift, + # :devmajor => fields.shift.oct, + # :devminor => fields.shift.oct, + # :prefix => fields.shift, + + # :empty => empty + end + + def initialize(vals) + unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode] then + 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 do |name| + instance_variable_set "@#{name}", vals[name] + end + + @empty = vals[:empty] + end + + def empty? + @empty + end + + def ==(other) + self.class === other and + @checksum == other.checksum and + @devmajor == other.devmajor and + @devminor == other.devminor and + @gid == other.gid and + @gname == other.gname and + @linkname == other.linkname and + @magic == other.magic and + @mode == other.mode and + @mtime == other.mtime and + @name == other.name and + @prefix == other.prefix and + @size == other.size and + @typeflag == other.typeflag and + @uid == other.uid and + @uname == other.uname and + @version == other.version + end + + def to_s + update_checksum + header + end + + def update_checksum + header = header " " * 8 + @checksum = oct calculate_checksum(header), 6 + end + + private + + def calculate_checksum(header) + header.unpack("C*").inject { |a, b| a + b } + end + + def header(checksum = @checksum) + header = [ + name, + oct(mode, 7), + oct(uid, 7), + oct(gid, 7), + oct(size, 11), + oct(mtime, 11), + checksum, + " ", + typeflag, + linkname, + magic, + oct(version, 2), + uname, + gname, + oct(devmajor, 7), + oct(devminor, 7), + prefix + ] + + header = header.pack PACK_FORMAT + + header << ("\0" * ((512 - header.size) % 512)) + end + + def oct(num, len) + "%0#{len}o" % num + end + +end + diff --git a/lib/rubygems/package/tar_input.rb b/lib/rubygems/package/tar_input.rb new file mode 100644 index 0000000000..2ed3d6b772 --- /dev/null +++ b/lib/rubygems/package/tar_input.rb @@ -0,0 +1,219 @@ +#++ +# Copyright (C) 2004 Mauricio Julio Fernández Pradier +# See LICENSE.txt for additional licensing information. +#-- + +require 'rubygems/package' + +class Gem::Package::TarInput + + include Gem::Package::FSyncDir + include Enumerable + + attr_reader :metadata + + private_class_method :new + + def self.open(io, security_policy = nil, &block) + is = new io, security_policy + + yield is + ensure + is.close if is + end + + def initialize(io, security_policy = nil) + @io = io + @tarreader = Gem::Package::TarReader.new @io + has_meta = false + + data_sig, meta_sig, data_dgst, meta_dgst = nil, nil, nil, nil + dgst_algo = security_policy ? Gem::Security::OPT[:dgst_algo] : nil + + @tarreader.each do |entry| + case entry.full_name + when "metadata" + @metadata = load_gemspec entry.read + has_meta = true + when "metadata.gz" + begin + # if we have a security_policy, then pre-read the metadata file + # and calculate it's digest + sio = nil + if security_policy + Gem.ensure_ssl_available + sio = StringIO.new(entry.read) + meta_dgst = dgst_algo.digest(sio.string) + sio.rewind + end + + 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 Gem::Package::FormatError, "No metadata found!" unless has_meta + end + + def close + @io.close + @tarreader.close + end + + def each(&block) + @tarreader.each do |entry| + next unless entry.full_name == "data.tar.gz" + is = zipped_stream entry + + begin + Gem::Package::TarReader.new is do |inner| + inner.each(&block) + end + ensure + is.close if is + end + end + + @tarreader.rewind + end + + def extract_entry(destdir, entry, expected_md5sum = nil) + if entry.directory? then + dest = File.join(destdir, entry.full_name) + + if File.dir? dest then + @fileops.chmod entry.header.mode, dest, :verbose=>false + else + @fileops.mkdir_p dest, :mode => entry.header.mode, :verbose => false + end + + fsync_dir dest + fsync_dir File.join(dest, "..") + + return + end + + # it's a file + md5 = Digest::MD5.new if expected_md5sum + destdir = File.join destdir, File.dirname(entry.full_name) + @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 + + open destfile, "wb", entry.header.mode do |os| + loop do + data = entry.read 4096 + break unless data + # HACK shouldn't we check the MD5 before writing to disk? + md5 << data if expected_md5sum + os.write(data) + end + + os.fsync + end + + @fileops.chmod entry.header.mode, destfile, :verbose => false + fsync_dir File.dirname(destfile) + fsync_dir File.join(File.dirname(destfile), "..") + + if expected_md5sum && expected_md5sum != md5.hexdigest then + raise Gem::Package::BadCheckSum + end + end + + # Attempt to YAML-load a gemspec from the given _io_ parameter. Return + # nil if it fails. + def load_gemspec(io) + Gem::Specification.from_yaml io + rescue Gem::Exception + nil + end + + ## + # Return an IO stream for the zipped entry. + # + # NOTE: Originally this method used two approaches, Return a GZipReader + # directly, or read the GZipReader into a string and return a StringIO on + # the string. The string IO approach was used for versions of ZLib before + # 1.2.1 to avoid buffer errors on windows machines. Then we found that + # errors happened with 1.2.1 as well, so we changed the condition. Then + # we discovered errors occurred with versions as late as 1.2.3. At this + # point (after some benchmarking to show we weren't seriously crippling + # the unpacking speed) we threw our hands in the air and declared that + # this method would use the String IO approach on all platforms at all + # times. And that's the way it is. + + def zipped_stream(entry) + if defined? Rubinius then + zis = Zlib::GzipReader.new entry + dis = zis.read + is = StringIO.new(dis) + else + # This is Jamis Buck's Zlib workaround for some unknown issue + entry.read(10) # skip the gzip header + zis = Zlib::Inflate.new(-Zlib::MAX_WBITS) + is = StringIO.new(zis.inflate(entry.read)) + end + ensure + zis.finish if zis + end + +end + diff --git a/lib/rubygems/package/tar_output.rb b/lib/rubygems/package/tar_output.rb new file mode 100644 index 0000000000..b22f7dd86b --- /dev/null +++ b/lib/rubygems/package/tar_output.rb @@ -0,0 +1,143 @@ +#++ +# Copyright (C) 2004 Mauricio Julio Fernández Pradier +# See LICENSE.txt for additional licensing information. +#-- + +require 'rubygems/package' + +## +# TarOutput is a wrapper to TarWriter that builds gem-format tar file. +# +# Gem-format tar files contain the following files: +# [data.tar.gz] A gzipped tar file containing the files that compose the gem +# which will be extracted into the gem/ dir on installation. +# [metadata.gz] A YAML format Gem::Specification. +# [data.tar.gz.sig] A signature for the gem's data.tar.gz. +# [metadata.gz.sig] A signature for the gem's metadata.gz. +# +# See TarOutput::open for usage details. + +class Gem::Package::TarOutput + + ## + # Creates a new TarOutput which will yield a TarWriter object for the + # data.tar.gz portion of a gem-format tar file. + # + # See #initialize for details on +io+ and +signer+. + # + # See #add_gem_contents for details on adding metadata to the tar file. + + def self.open(io, signer = nil, &block) # :yield: data_tar_writer + tar_outputter = new io, signer + tar_outputter.add_gem_contents(&block) + tar_outputter.add_metadata + tar_outputter.add_signatures + + ensure + tar_outputter.close + end + + ## + # Creates a new TarOutput that will write a gem-format tar file to +io+. If + # +signer+ is given, the data.tar.gz and metadata.gz will be signed and + # the signatures will be added to the tar file. + + def initialize(io, signer) + @io = io + @signer = signer + + @tar_writer = Gem::Package::TarWriter.new @io + + @metadata = nil + + @data_signature = nil + @meta_signature = nil + end + + ## + # Yields a TarWriter for the data.tar.gz inside a gem-format tar file. + # The yielded TarWriter has been extended with a #metadata= method for + # attaching a YAML format Gem::Specification which will be written by + # add_metadata. + + def add_gem_contents + @tar_writer.add_file "data.tar.gz", 0644 do |inner| + sio = @signer ? StringIO.new : nil + Zlib::GzipWriter.wrap(sio || inner) do |os| + + Gem::Package::TarWriter.new os do |data_tar_writer| + def data_tar_writer.metadata() @metadata end + def data_tar_writer.metadata=(metadata) @metadata = metadata end + + yield data_tar_writer + + @metadata = data_tar_writer.metadata + end + end + + # if we have a signing key, then sign the data + # digest and return the signature + if @signer then + digest = Gem::Security::OPT[:dgst_algo].digest sio.string + @data_signature = @signer.sign digest + inner.write sio.string + end + end + + self + end + + ## + # Adds metadata.gz to the gem-format tar file which was saved from a + # previous #add_gem_contents call. + + def add_metadata + return if @metadata.nil? + + @tar_writer.add_file "metadata.gz", 0644 do |io| + begin + sio = @signer ? StringIO.new : nil + gzos = Zlib::GzipWriter.new(sio || io) + gzos.write @metadata + ensure + gzos.flush + gzos.finish + + # if we have a signing key, then sign the metadata digest and return + # the signature + if @signer then + digest = Gem::Security::OPT[:dgst_algo].digest sio.string + @meta_signature = @signer.sign digest + io.write sio.string + end + end + end + end + + ## + # Adds data.tar.gz.sig and metadata.gz.sig to the gem-format tar files if + # a Gem::Security::Signer was sent to initialize. + + def add_signatures + if @data_signature then + @tar_writer.add_file 'data.tar.gz.sig', 0644 do |io| + io.write @data_signature + end + end + + if @meta_signature then + @tar_writer.add_file 'metadata.gz.sig', 0644 do |io| + io.write @meta_signature + end + end + end + + ## + # Closes the TarOutput. + + def close + @tar_writer.close + end + +end + diff --git a/lib/rubygems/package/tar_reader.rb b/lib/rubygems/package/tar_reader.rb new file mode 100644 index 0000000000..8359399207 --- /dev/null +++ b/lib/rubygems/package/tar_reader.rb @@ -0,0 +1,86 @@ +#++ +# Copyright (C) 2004 Mauricio Julio Fernández Pradier +# See LICENSE.txt for additional licensing information. +#-- + +require 'rubygems/package' + +class Gem::Package::TarReader + + include Gem::Package + + class UnexpectedEOF < StandardError; end + + def self.new(io) + reader = super + + return reader unless block_given? + + begin + yield reader + ensure + reader.close + end + + nil + end + + def initialize(io) + @io = io + @init_pos = io.pos + end + + def close + end + + def each + loop do + return if @io.eof? + + header = Gem::Package::TarHeader.from @io + return if header.empty? + + entry = Gem::Package::TarReader::Entry.new header, @io + size = entry.header.size + + yield entry + + skip = (512 - (size % 512)) % 512 + + if @io.respond_to? :seek then + # avoid reading... + @io.seek(size - entry.bytes_read, IO::SEEK_CUR) + else + pending = size - entry.bytes_read + + while pending > 0 do + 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 + + alias each_entry each + + ## + # NOTE: Do not call #rewind during #each + + def rewind + if @init_pos == 0 then + raise Gem::Package::NonSeekableIO unless @io.respond_to? :rewind + @io.rewind + else + raise Gem::Package::NonSeekableIO unless @io.respond_to? :pos= + @io.pos = @init_pos + end + end + +end + diff --git a/lib/rubygems/package/tar_reader/entry.rb b/lib/rubygems/package/tar_reader/entry.rb new file mode 100644 index 0000000000..dcc66153d8 --- /dev/null +++ b/lib/rubygems/package/tar_reader/entry.rb @@ -0,0 +1,99 @@ +#++ +# Copyright (C) 2004 Mauricio Julio Fernández Pradier +# See LICENSE.txt for additional licensing information. +#-- + +require 'rubygems/package' + +class Gem::Package::TarReader::Entry + + attr_reader :header + + def initialize(header, io) + @closed = false + @header = header + @io = io + @orig_pos = @io.pos + @read = 0 + end + + def check_closed # :nodoc: + raise IOError, "closed #{self.class}" if closed? + end + + def bytes_read + @read + end + + def close + @closed = true + end + + def closed? + @closed + end + + def eof? + check_closed + + @read >= @header.size + end + + def full_name + if @header.prefix != "" then + File.join @header.prefix, @header.name + else + @header.name + end + end + + def getc + check_closed + + return nil if @read >= @header.size + + ret = @io.getc + @read += 1 if ret + + ret + end + + def directory? + @header.typeflag == "5" + end + + def file? + @header.typeflag == "0" + end + + def pos + check_closed + + bytes_read + end + + def read(len = nil) + check_closed + + return nil if @read >= @header.size + + len ||= @header.size - @read + max_read = [len, @header.size - @read].min + + ret = @io.read max_read + @read += ret.size + + ret + end + + def rewind + check_closed + + raise Gem::Package::NonSeekableIO unless @io.respond_to? :pos= + + @io.pos = @orig_pos + @read = 0 + end + +end + diff --git a/lib/rubygems/package/tar_writer.rb b/lib/rubygems/package/tar_writer.rb new file mode 100644 index 0000000000..6e11440e22 --- /dev/null +++ b/lib/rubygems/package/tar_writer.rb @@ -0,0 +1,180 @@ +#++ +# Copyright (C) 2004 Mauricio Julio Fernández Pradier +# See LICENSE.txt for additional licensing information. +#-- + +require 'rubygems/package' + +class Gem::Package::TarWriter + + class FileOverflow < 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(io) + @io = io + end + + def write(data) + @io.write data + end + + end + + def self.new(io) + writer = super + + return writer unless block_given? + + begin + yield writer + ensure + writer.close + end + + nil + end + + def initialize(io) + @io = io + @closed = false + end + + def add_file(name, mode) + check_closed + + raise Gem::Package::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) if block_given? + + size = @io.pos - init_pos - 512 + + remainder = (512 - (size % 512)) % 512 + @io.write "\0" * remainder + + final_pos = @io.pos + @io.pos = init_pos + + header = Gem::Package::TarHeader.new :name => name, :mode => mode, + :size => size, :prefix => prefix + + @io.write header + @io.pos = final_pos + + self + end + + def add_file_simple(name, mode, size) + check_closed + + name, prefix = split_name name + + header = Gem::Package::TarHeader.new(:name => name, :mode => mode, + :size => size, :prefix => prefix).to_s + + @io.write header + os = BoundedStream.new @io, size + + yield os if block_given? + + min_padding = size - os.written + @io.write("\0" * min_padding) + + remainder = (512 - (size % 512)) % 512 + @io.write("\0" * remainder) + + self + end + + def check_closed + raise IOError, "closed #{self.class}" if closed? + end + + def close + check_closed + + @io.write "\0" * 1024 + flush + + @closed = true + end + + def closed? + @closed + end + + def flush + check_closed + + @io.flush if @io.respond_to? :flush + end + + def mkdir(name, mode) + check_closed + + name, prefix = split_name(name) + + header = Gem::Package::TarHeader.new :name => name, :mode => mode, + :typeflag => "5", :size => 0, + :prefix => prefix + + @io.write header + + self + end + + def split_name(name) # :nodoc: + raise Gem::Package::TooLongFileName if name.size > 256 + + if name.size <= 100 then + 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 + + if name.size > 100 or prefix.size > 155 then + raise Gem::Package::TooLongFileName + end + end + + return name, prefix + end + +end + diff --git a/lib/rubygems/remote_fetcher.rb b/lib/rubygems/remote_fetcher.rb index cb22e1f1b1..f49ee2f4a1 100644 --- a/lib/rubygems/remote_fetcher.rb +++ b/lib/rubygems/remote_fetcher.rb @@ -2,7 +2,6 @@ require 'net/http' require 'uri' require 'rubygems' -require 'rubygems/gem_open_uri' ## # RemoteFetcher handles the details of fetching gems and gem information from @@ -10,6 +9,8 @@ require 'rubygems/gem_open_uri' class Gem::RemoteFetcher + include Gem::UserInteraction + class FetchError < Gem::Exception; end @fetcher = nil @@ -29,6 +30,10 @@ class Gem::RemoteFetcher # HTTP_PROXY_PASS) # * :no_proxy: ignore environment variables and _don't_ use a proxy def initialize(proxy) + Socket.do_not_reverse_lookup = true + + @connections = {} + @requests = Hash.new 0 @proxy_uri = case proxy when :no_proxy then nil @@ -38,6 +43,65 @@ class Gem::RemoteFetcher 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, install_dir = Gem.dir) + 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 + begin + 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 + rescue Gem::RemoteFetcher::FetchError + raise if spec.original_platform == spec.platform + + alternate_name = "#{spec.original_name}.gem" + + say "Failed, downloading gem #{alternate_name}" if + Gem.configuration.really_verbose + + remote_gem_path = source_uri + "gems/#{alternate_name}" + + gem = Gem::RemoteFetcher.fetcher.fetch_path remote_gem_path + end + + 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 + # Downloads +uri+. def fetch_path(uri) open_uri_or_path(uri) do |input| @@ -47,9 +111,8 @@ class Gem::RemoteFetcher raise FetchError, "timed out fetching #{uri}" rescue IOError, SocketError, SystemCallError => e raise FetchError, "#{e.class}: #{e} reading #{uri}" - rescue OpenURI::HTTPError => e - body = e.io.readlines.join "\n\t" - message = "#{e.class}: #{e} reading #{uri}\n\t#{body}" + rescue => e + message = "#{e.class}: #{e} reading #{uri}" raise FetchError, message end @@ -83,7 +146,8 @@ class Gem::RemoteFetcher end rescue SocketError, SystemCallError, Timeout::Error => e - raise FetchError, "#{e.message} (#{e.class})\n\tgetting size of #{uri}" + raise Gem::RemoteFetcher::FetchError, + "#{e.message} (#{e.class})\n\tgetting size of #{uri}" end private @@ -131,26 +195,77 @@ class Gem::RemoteFetcher # 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) + def open_uri_or_path(uri, depth = 0, &block) if file_uri?(uri) open(get_file_uri_path(uri), &block) else - connection_options = { - "User-Agent" => "RubyGems/#{Gem::RubyGemsVersion} #{Gem::Platform.local}" - } + uri = URI.parse uri unless URI::Generic === uri + net_http_args = [uri.host, uri.port] + + if @proxy_uri then + net_http_args += [ @proxy_uri.host, + @proxy_uri.port, + @proxy_uri.user, + @proxy_uri.password + ] + end + + connection_id = net_http_args.join ':' + @connections[connection_id] ||= Net::HTTP.new(*net_http_args) + connection = @connections[connection_id] - 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)||''] + if uri.scheme == 'https' && ! connection.started? + http_obj.use_ssl = true + http_obj.verify_mode = OpenSSL::SSL::VERIFY_NONE end - uri = URI.parse uri unless URI::Generic === uri + connection.start unless connection.started? + + request = Net::HTTP::Get.new(uri.request_uri) unless uri.nil? || uri.user.nil? || uri.user.empty? then - connection_options[:http_basic_authentication] = - [unescape(uri.user), unescape(uri.password)] + request.basic_auth(uri.user, uri.password) end - open(uri, connection_options, &block) + ua = "RubyGems/#{Gem::RubyGemsVersion} #{Gem::Platform.local}" + ua << " Ruby/#{RUBY_VERSION} (#{RUBY_RELEASE_DATE}" + ua << " patchlevel #{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL + ua << ")" + + request.add_field 'User-Agent', ua + request.add_field 'Connection', 'keep-alive' + request.add_field 'Keep-Alive', '30' + + # HACK work around EOFError bug in Net::HTTP + retried = false + begin + @requests[connection_id] += 1 + response = connection.request(request) + rescue EOFError + requests = @requests[connection_id] + say "connection reset after #{requests} requests, retrying" if + Gem.configuration.really_verbose + + raise Gem::RemoteFetcher::FetchError, 'too many connection resets' if + retried + + @requests[connection_id] = 0 + + connection.finish + connection.start + retried = true + retry + end + + case response + when Net::HTTPOK then + block.call(StringIO.new(response.body)) if block + when Net::HTTPRedirection then + raise Gem::RemoteFetcher::FetchError, "too many redirects" if depth > 10 + open_uri_or_path(response['Location'], depth + 1, &block) + else + raise Gem::RemoteFetcher::FetchError, + "bad response #{response.message} #{response.code}" + end end end diff --git a/lib/rubygems/requirement.rb b/lib/rubygems/requirement.rb index 4dfba4fa61..209bd432f0 100644 --- a/lib/rubygems/requirement.rb +++ b/lib/rubygems/requirement.rb @@ -16,6 +16,8 @@ class Gem::Requirement include Comparable + attr_reader :requirements + OPS = { "=" => lambda { |v, r| v == r }, "!=" => lambda { |v, r| v != r }, diff --git a/lib/rubygems/rubygems_version.rb b/lib/rubygems/rubygems_version.rb index 146e2cfee4..cc7269d622 100644 --- a/lib/rubygems/rubygems_version.rb +++ b/lib/rubygems/rubygems_version.rb @@ -2,5 +2,5 @@ # This file is auto-generated by build scripts. # See: rake update_version module Gem - RubyGemsVersion = '1.0.1' + RubyGemsVersion = '1.1.0' end diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb index 415e98ffc0..345d36bbd3 100644 --- a/lib/rubygems/security.rb +++ b/lib/rubygems/security.rb @@ -4,6 +4,7 @@ # See LICENSE.txt for permissions. #++ +require 'rubygems' require 'rubygems/gem_openssl' # = Signed Gems README diff --git a/lib/rubygems/server.rb b/lib/rubygems/server.rb index ed957dd38a..dab449894f 100644 --- a/lib/rubygems/server.rb +++ b/lib/rubygems/server.rb @@ -1,7 +1,7 @@ require 'webrick' -require 'rdoc/template' require 'yaml' require 'zlib' +require 'erb' require 'rubygems' @@ -27,107 +27,87 @@ 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 -
- + DOC_TEMPLATE = <<-'WEBPAGE' + + + + + + RubyGems Documentation Index + + + + +
+

RubyGems Documentation Index

+
+ + +
+
+
+

Summary

+

There are <%=values["gem_count"]%> gems installed:

+

+ <%= values["specs"].map { |v| "#{v["name"]}" }.join ', ' %>. +

Gems

+ +
+ <% values["specs"].each do |spec| %> +
+ <% if spec["first_name_entry"] then %> + "> + <% end %> + + <%=spec["name"]%> <%=spec["version"]%> + + <% if spec["rdoc_installed"] then %> + ">[rdoc] + <% else %> + [rdoc] + <% end %> + + <% if spec["homepage"] then %> + " title="<%=spec["homepage"]%>">[www] + <% else %> + [www] + <% end %> + + <% if spec["has_deps"] then %> + - depends on + <%= spec["dependencies"].map { |v| "#{v["name"]}" }.join ', ' %>. + <% end %> +
+
+ <%=spec["summary"]%> + <% if spec["executables"] then %> +
+ + <% if spec["only_one_executable"] then %> + Executable is + <% else %> + Executables are + <%end%> + + <%= spec["executables"].map { |v| "#{v["executable"]}"}.join ', ' %>. + + <%end%> +
+
+
+ <% end %> +
+ +
+
-
+ - - - + + WEBPAGE # CSS is copy & paste from rdoc-style.css, RDoc V1.0.1 - 20041108 @@ -496,11 +476,12 @@ div.method-source-code pre { color: #ffdead; overflow: hidden; } end # create page from template - template = TemplatePage.new(DOC_TEMPLATE) + template = ERB.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 + values = { "gem_count" => specs.size.to_s, "specs" => specs, + "total_file_count" => total_file_count.to_s } + result = template.result binding + res.body = result end paths = { "/gems" => "/cache/", "/doc_root" => "/doc/" } diff --git a/lib/rubygems/source_index.rb b/lib/rubygems/source_index.rb index 1283f8f904..61f5324a95 100644 --- a/lib/rubygems/source_index.rb +++ b/lib/rubygems/source_index.rb @@ -8,437 +8,512 @@ 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. - # 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 +class Gem::SourceIndex - include Enumerable + include Enumerable + include Gem::UserInteraction + + class << self 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) # HACK warn - 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") } + ## + # 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) # HACK warn end + 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}'" + ## + # 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 + + ## + # Creates a new SourceIndex from the ruby format gem specifications in + # +spec_dirs+. + + def from_gems_in(*spec_dirs) + self.new.load_gems_in(*spec_dirs) + end + + ## + # Loads a ruby-format specification from +file_name+ and returns the + # loaded spec. + + 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 - return nil + 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 - # Instance Methods ----------------------------------------------- + end - # 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 + ## + # 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 specifications in +spec_dirs+. + + def load_gems_in(*spec_dirs) + @gems.clear + + spec_dirs.reverse_each do |spec_dir| + spec_files = Dir.glob File.join(spec_dir, '*.gemspec') + + spec_files.each do |spec_file| + gemspec = self.class.load_specification spec_file.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 + end - 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] + ## + # Returns a Hash of name => Specification of the latest versions of each + # gem in this index. - next unless prev_ver.nil? or curr_ver >= prev_ver + def latest_specs + result = Hash.new { |h,k| h[k] = [] } + latest = {} - if prev_ver.nil? or curr_ver > prev_ver then - result[name].clear - latest[name] = curr_ver - end + sort.each do |_, spec| + name = spec.name + curr_ver = spec.version + prev_ver = latest.key?(name) ? latest[name].version : nil + + next unless prev_ver.nil? or curr_ver >= prev_ver or + latest[name].platform != Gem::Platform::RUBY - result[name] << spec + if prev_ver.nil? or + (curr_ver > prev_ver and spec.platform == Gem::Platform::RUBY) then + result[name].clear + latest[name] = spec end - result.values.flatten - end + if spec.platform != Gem::Platform::RUBY then + result[name].delete_if do |result_spec| + result_spec.platform == spec.platform + end + end - # Add a gem specification to the source index. - def add_spec(gem_spec) - @gems[gem_spec.full_name] = gem_spec + result[name] << spec end - # Remove a gem specification named +full_name+. - def remove_spec(full_name) - @gems.delete(full_name) - end + result.values.flatten + end - # Iterate over the specifications in the source index. - def each(&block) # :yields: gem.full_name, gem - @gems.each(&block) - end + ## + # Add a gem specification to the source index. - # The gem specification given a full gem spec name. - def specification(full_name) - @gems[full_name] - end + def add_spec(gem_spec) + @gems[gem_spec.full_name] = gem_spec + end - # The signature for the source index. Changes in the signature - # indicate a change in the index. - def index_signature - require 'rubygems/digest/sha2' + ## + # Add gem specifications to the source index. - Gem::SHA256.new.hexdigest(@gems.keys.sort.join(',')).to_s + def add_specs(*gem_specs) + gem_specs.each do |spec| + add_spec spec end + end - # The signature for the given gem specification. - def gem_signature(gem_full_name) - require 'rubygems/digest/sha2' + ## + # Remove a gem specification named +full_name+. - Gem::SHA256.new.hexdigest(@gems[gem_full_name].to_yaml).to_s - end + 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' - def size - @gems.size + Gem::SHA256.new.hexdigest(@gems[gem_full_name].to_yaml).to_s + end + + def size + @gems.size + end + alias length size + + ## + # Find a gem by an exact match on the short name. + + def find_name(gem_name, version_requirement = Gem::Requirement.default) + search(/^#{gem_name}$/, version_requirement) + end + + ## + # Search for a gem by Gem::Dependency +gem_pattern+. If +only_platform+ + # is true, only gems matching Gem::Platform.local will be returned. An + # Array of matching Gem::Specification objects is returned. + # + # For backwards compatibility, a String or Regexp pattern may be passed as + # +gem_pattern+, and a Gem::Requirement for +platform_only+. This + # behavior is deprecated and will be removed. + + def search(gem_pattern, platform_only = false) + version_requirement = nil + only_platform = false + + case gem_pattern # TODO warn after 2008/03, remove three months after + when Regexp then + version_requirement = platform_only || Gem::Requirement.default + when Gem::Dependency then + only_platform = platform_only + version_requirement = gem_pattern.version_requirements + gem_pattern = if gem_pattern.name.empty? then + // + else + /^#{Regexp.escape gem_pattern.name}$/ + end + else + version_requirement = platform_only || Gem::Requirement.default + gem_pattern = /#{gem_pattern}/i end - alias length size - # 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) + unless Gem::Requirement === version_requirement then + version_requirement = Gem::Requirement.create version_requirement end - # Search for a gem by Gem::Dependency +gem_pattern+. If +only_platform+ - # is true, only gems matching Gem::Platform.local will be returned. An - # Array of matching Gem::Specification objects is returned. - # - # For backwards compatibility, a String or Regexp pattern may be passed as - # +gem_pattern+, and a Gem::Requirement for +platform_only+. This - # behavior is deprecated and will be removed. - def search(gem_pattern, platform_only = false) - version_requirement = nil - only_platform = false - - case gem_pattern # TODO warn after 2008/03, remove three months after - when Regexp then - version_requirement = platform_only || Gem::Requirement.default - when Gem::Dependency then - only_platform = platform_only - version_requirement = gem_pattern.version_requirements - gem_pattern = if gem_pattern.name.empty? then - // - else - /^#{Regexp.escape gem_pattern.name}$/ - end - else - version_requirement = platform_only || Gem::Requirement.default - gem_pattern = /#{gem_pattern}/i - end + specs = @gems.values.select do |spec| + spec.name =~ gem_pattern and + version_requirement.satisfied_by? spec.version + end - unless Gem::Requirement === version_requirement then - version_requirement = Gem::Requirement.create version_requirement + if only_platform then + specs = specs.select do |spec| + Gem::Platform.match spec.platform end + end - specs = @gems.values.select do |spec| - spec.name =~ gem_pattern and - version_requirement.satisfied_by? spec.version - end + specs.sort_by { |s| s.sort_obj } + end - if only_platform then - specs = specs.select do |spec| - Gem::Platform.match spec.platform - end - end + ## + # Refresh the source index from the local file system. + # + # return:: Returns a pointer to itself. - specs.sort_by { |s| s.sort_obj } - end + def refresh! + load_gems_in(self.class.installed_spec_directories) + 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. - # Returns an Array of Gem::Specifications that are not up to date. - # - def outdated - dep = Gem::Dependency.new '', Gem::Requirement.default + def outdated + dep = Gem::Dependency.new '', Gem::Requirement.default - remotes = Gem::SourceInfoCache.search dep, true + remotes = Gem::SourceInfoCache.search dep, true - outdateds = [] + 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 + latest_specs.each do |local| + name = local.name + remote = remotes.select { |spec| spec.name == name }. + sort_by { |spec| spec.version.to_ints }. + last - outdateds + outdateds << name if remote and local.version < remote.version end - def update(source_uri) - use_incremental = false + outdateds + end - begin - gem_names = fetch_quick_index source_uri - remove_extra gem_names - missing_gems = find_missing gem_names + ## + # Updates this SourceIndex from +source_uri+. If +all+ is false, only the + # latest gems are fetched. - return false if missing_gems.size.zero? + def update(source_uri, all) + source_uri = URI.parse source_uri unless URI::Generic === source_uri + source_uri.path += '/' unless source_uri.path =~ /\/$/ - say "missing #{missing_gems.size} gems" if - missing_gems.size > 0 and Gem.configuration.really_verbose + use_incremental = false - 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 + begin + gem_names = fetch_quick_index source_uri, all + remove_extra gem_names + missing_gems = find_missing gem_names - 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 + return false if missing_gems.size.zero? - true - end + say "Missing metadata for #{missing_gems.size} gems" if + missing_gems.size > 0 and Gem.configuration.really_verbose - def ==(other) # :nodoc: - self.class === other and @gems == other.gems + 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 - def dump - Marshal.dump(self) + 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 - protected + true + end - attr_reader :gems + def ==(other) # :nodoc: + self.class === other and @gems == other.gems + end - private + def dump + Marshal.dump(self) + end - def fetcher - require 'rubygems/remote_fetcher' + protected - Gem::RemoteFetcher.fetcher - end + attr_reader :gems - def fetch_index_from(source_uri) - @fetch_error = nil + private + + def fetcher + require 'rubygems/remote_fetcher' + + Gem::RemoteFetcher.fetcher + end + + def fetch_index_from(source_uri) + @fetch_error = nil - indexes = %W[ + 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 + indexes.each do |name| + spec_data = nil + index = source_uri + name + begin + spec_data = fetcher.fetch_path index + 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 - nil end - def fetch_bulk_index(source_uri) - say "Bulk updating Gem source index for: #{source_uri}" + nil + end - index = fetch_index_from(source_uri) - if index.nil? then - raise Gem::RemoteSourceException, + 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 + @fetch_error = nil + index + end + + ## + # Get the quick index needed for incremental updates. + + def fetch_quick_index(source_uri, all) + index = all ? 'index' : 'latest_index' - # 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 + zipped_index = fetcher.fetch_path source_uri + "quick/#{index}.rz" + + unzip(zipped_index).split("\n") + rescue ::Exception => e + unless all then + say "Latest index not found, using quick index" if + Gem.configuration.really_verbose + + fetch_quick_index source_uri, true + else raise Gem::OperationNotSupportedError, - "No quick index found: " + ex.message + "No quick index found: #{e.message}" end + 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 + ## + # Make a list of full names for all the missing gemspecs. - 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 + def find_missing(spec_names) + spec_names.find_all { |full_name| + specification(full_name).nil? + } + end - # Unzip the given string. - def unzip(string) - require 'zlib' - Zlib::Inflate.inflate(string) + 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 - # 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 + ## + # 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 + 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 - 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, + 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" \ + 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 + else + add_spec gemspec + progress.updated spec_name end - progress.done - progress.count + @fetch_error = nil end - + progress.done + progress.count end - # Cache is an alias for SourceIndex to allow older YAMLized source - # index objects to load properly. +end + +module Gem + + # :stopdoc: + + # Cache is an alias for SourceIndex to allow older YAMLized source index + # objects to load properly. Cache = SourceIndex + # :starddoc: + end diff --git a/lib/rubygems/source_info_cache.rb b/lib/rubygems/source_info_cache.rb index c84868a5f5..9383f6362e 100644 --- a/lib/rubygems/source_info_cache.rb +++ b/lib/rubygems/source_info_cache.rb @@ -4,6 +4,7 @@ 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: @@ -25,7 +26,7 @@ require 'rubygems/user_interaction' # @source_index => Gem::SourceIndex # ... # } -# + class Gem::SourceInfoCache include Gem::UserInteraction @@ -37,7 +38,7 @@ class Gem::SourceInfoCache def self.cache return @cache if @cache @cache = new - @cache.refresh if Gem.configuration.update_sources + @cache.refresh false if Gem.configuration.update_sources @cache end @@ -45,86 +46,178 @@ class Gem::SourceInfoCache cache.cache_data end - # Search all source indexes for +pattern+. - def self.search(pattern, platform_only = false) - cache.search pattern, platform_only + ## + # The name of the system cache file. + + def self.latest_system_cache_file + File.join File.dirname(system_cache_file), + "latest_#{File.basename system_cache_file}" + end + + ## + # The name of the latest user cache file. + + def self.latest_user_cache_file + File.join File.dirname(user_cache_file), + "latest_#{File.basename user_cache_file}" + end + + ## + # Search all source indexes. See Gem::SourceInfoCache#search. + + def self.search(*args) + cache.search(*args) 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) + ## + # Search all source indexes returning the source_uri. See + # Gem::SourceInfoCache#search_with_source. + + def self.search_with_source(*args) + cache.search_with_source(*args) + end + + ## + # The name of the system cache file. (class method) + + def self.system_cache_file + @system_cache_file ||= Gem.default_system_source_cache_dir + end + + ## + # The name of the user cache file. + + def self.user_cache_file + @user_cache_file ||= + ENV['GEMCACHE'] || Gem.default_user_source_cache_dir end def initialize # :nodoc: @cache_data = nil @cache_file = nil @dirty = false + @only_latest = true 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 + @only_latest = true - sice = Gem::SourceInfoCacheEntry.new Gem::SourceIndex.new, 0 - sice.refresh url # HACK may be unnecessary, see ::cache and #refresh + @cache_data = read_cache_data latest_cache_file - @cache_data[url] = sice @cache_data end - def reset_cache_data - @cache_data = {} - end + ## + # The name of the cache file. - # 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 + + ## + # Force cache file to be reset, useful for integration testing of rubygems + + def reset_cache_file + @cache_file = nil + 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 + def latest_cache_data + latest_cache_data = {} + + cache_data.each do |repo, sice| + latest = sice.source_index.latest_specs + + new_si = Gem::SourceIndex.new + new_si.add_specs(*latest) + + latest_sice = Gem::SourceInfoCacheEntry.new new_si, sice.size + latest_cache_data[repo] = latest_sice + end + + latest_cache_data + end + + ## + # The name of the latest cache file. + + def latest_cache_file + File.join File.dirname(cache_file), "latest_#{File.basename cache_file}" + end + + ## + # The name of the latest system cache file. + + def latest_system_cache_file + self.class.latest_system_cache_file + end + + ## + # The name of the latest user cache file. + + def latest_user_cache_file + self.class.latest_user_cache_file + end + + def read_all_cache_data + if @only_latest then + @only_latest = false + @cache_data = read_cache_data cache_file + end + end + + def read_cache_data(file) + # Marshal loads 30-40% faster from a String, and 2MB on 20061116 is small + data = open 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, cache_data + end + end + + cache_data + rescue => e + if Gem.configuration.really_verbose then + say "Exception during cache_data handling: #{e.class} - #{e}" + say "Cache file was: #{file}" + say "\t#{e.backtrace.join "\n\t"}" + end + + {} + end + + ## + # Refreshes each source in the cache from its repository. If +all+ is + # false, only latest gems are updated. + + def refresh(all) Gem.sources.each do |source_uri| cache_entry = cache_data[source_uri] if cache_entry.nil? then @@ -132,14 +225,34 @@ class Gem::SourceInfoCache cache_data[source_uri] = cache_entry end - update if cache_entry.refresh source_uri + update if cache_entry.refresh source_uri, all end flush end - # Searches all source indexes for +pattern+. - def search(pattern, platform_only = false) + def reset_cache_for(url, cache_data) + say "Reseting cache for #{url}" if Gem.configuration.really_verbose + + sice = Gem::SourceInfoCacheEntry.new Gem::SourceIndex.new, 0 + sice.refresh url, false # HACK may be unnecessary, see ::cache and #refresh + + cache_data[url] = sice + cache_data + end + + def reset_cache_data + @cache_data = nil + end + + ## + # Searches all source indexes. See Gem::SourceIndex#search for details on + # +pattern+ and +platform_only+. If +all+ is set to true, the full index + # will be loaded before searching. + + def search(pattern, platform_only = false, all = false) + read_all_cache_data if all + cache_data.map do |source_uri, sic_entry| next unless Gem.sources.include? source_uri sic_entry.source_index.search pattern, platform_only @@ -150,7 +263,9 @@ class Gem::SourceInfoCache # 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) + def search_with_source(pattern, only_platform = false, all = false) + read_all_cache_data if all + results = [] cache_data.map do |source_uri, sic_entry| @@ -164,68 +279,75 @@ class Gem::SourceInfoCache results end - # Mark the cache as updated (i.e. dirty). - def update - @dirty = true + ## + # 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 + ## # 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 ||= Gem.default_system_source_cache_dir + ## + # Determine if +path+ is a candidate for a cache file. Returns +path+ if + # it is, nil if not. + + def try_file(path) + return path if File.writable? path + return nil if File.exist? path + + dir = File.dirname path + + unless File.exist? dir then + begin + FileUtils.mkdir_p dir + rescue RuntimeError, SystemCallError + return nil + end + end + + if File.writable? dir then + open path, "wb" do |io| io.write Marshal.dump({}) end + return path + end + + nil + end + + ## + # Mark the cache as updated (i.e. dirty). + + def update + @dirty = true 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'] || Gem.default_user_source_cache_dir - end - + ## # Write data to the proper cache. + def write_cache - open cache_file, "wb" do |f| - f.write Marshal.dump(cache_data) + open cache_file, 'wb' do |io| + io.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, SystemCallError - return nil - end + open latest_cache_file, 'wb' do |io| + io.write Marshal.dump(latest_cache_data) 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 index 02e03ca9db..c3f75e5b99 100644 --- a/lib/rubygems/source_info_cache_entry.rb +++ b/lib/rubygems/source_info_cache_entry.rb @@ -3,24 +3,31 @@ require 'rubygems/source_index' require 'rubygems/remote_fetcher' ## -# Entrys held by a SourceInfoCache. +# Entries 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 + @all = false end - def refresh(source_uri) + def refresh(source_uri, all) begin marshal_uri = URI.join source_uri.to_s, "Marshal.#{Gem.marshal_version}" remote_size = Gem::RemoteFetcher.fetcher.fetch_size marshal_uri @@ -29,9 +36,12 @@ class Gem::SourceInfoCacheEntry 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 + # TODO Use index_signature instead of size? + return false if @size == remote_size and @all + + updated = @source_index.update source_uri, all @size = remote_size + @all = all updated end diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb index be03150c96..de37a08b60 100644 --- a/lib/rubygems/specification.rb +++ b/lib/rubygems/specification.rb @@ -13,7 +13,7 @@ require 'rubygems/platform' if RUBY_VERSION < '1.9' then def Time.today t = Time.now - t - ((t.to_i + t.gmt_offset) % 86400) + t - ((t.to_f + t.gmt_offset) % 86400) end unless defined? Time.today end # :startdoc: diff --git a/lib/rubygems/uninstaller.rb b/lib/rubygems/uninstaller.rb index 1c979d8573..e2b5e5372b 100644 --- a/lib/rubygems/uninstaller.rb +++ b/lib/rubygems/uninstaller.rb @@ -12,7 +12,7 @@ require 'rubygems/user_interaction' ## # An Uninstaller. -# + class Gem::Uninstaller include Gem::UserInteraction @@ -21,8 +21,8 @@ class Gem::Uninstaller # Constructs an Uninstaller instance # # gem:: [String] The Gem name to uninstall - # - def initialize(gem, options) + + def initialize(gem, options = {}) @gem = gem @version = options[:version] || Gem::Requirement.default gem_home = options[:install_dir] || Gem.dir @@ -30,12 +30,13 @@ class Gem::Uninstaller @force_executables = options[:executables] @force_all = options[:all] @force_ignore = options[:ignore] + @bin_dir = options[:bin_dir] 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) @@ -66,18 +67,14 @@ class Gem::Uninstaller 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. - # + # Removes installed executables and batch files (windows only) for + # +gemspec+. + def remove_executables(gemspec) return if gemspec.nil? if gemspec.executables.size > 0 then - bindir = Gem.bindir @gem_home - - raise Gem::FilePermissionError, bindir unless File.writable? bindir + bindir = @bin_dir ? @bin_dir : (Gem.bindir @gem_home) list = Gem.source_index.search(gemspec.name).delete_if { |spec| spec.version == gemspec.version @@ -93,14 +90,19 @@ class Gem::Uninstaller return if executables.size == 0 - answer = @force_executables || ask_yes_no( - "Remove executables:\n" \ - "\t#{gemspec.executables.join(", ")}\n\nin addition to the gem?", - true) # " # appease ruby-mode - don't ask + answer = if @force_executables.nil? then + ask_yes_no("Remove executables:\n" \ + "\t#{gemspec.executables.join(", ")}\n\nin addition to the gem?", + true) # " # appease ruby-mode - don't ask + else + @force_executables + end unless answer then say "Executables and scripts will remain installed." else + raise Gem::FilePermissionError, bindir unless File.writable? bindir + gemspec.executables.each do |exe_name| say "Removing #{exe_name}" FileUtils.rm_f File.join(bindir, exe_name) @@ -110,23 +112,22 @@ class Gem::Uninstaller end end + ## + # Removes all gems in +list+. # - # 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. - # + # NOTE: removes uninstalled gems from +list+. + def remove_all(list) - list.dup.each { |gem| remove(gem, list) } + list.dup.each { |spec| remove spec, 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 dependencies_ok? spec then raise Gem::DependencyRemovalException, @@ -134,10 +135,11 @@ class Gem::Uninstaller end unless path_ok? spec then - alert("In order to remove #{spec.name}, please execute:\n" \ - "\tgem uninstall #{spec.name} --install-dir=#{spec.installation_path}") - raise Gem::GemNotInHomeException, + e = Gem::GemNotInHomeException.new \ "Gem is not installed in directory #{@gem_home}" + e.spec = spec + + raise e end raise Gem::FilePermissionError, spec.installation_path unless @@ -182,8 +184,8 @@ class Gem::Uninstaller def dependencies_ok?(spec) return true if @force_ignore - srcindex = Gem::SourceIndex.from_installed_gems - deplist = Gem::DependencyList.from_source_index srcindex + source_index = Gem::SourceIndex.from_installed_gems + deplist = Gem::DependencyList.from_source_index source_index deplist.ok_to_remove?(spec.full_name) || ask_if_ok(spec) end diff --git a/lib/rubygems/user_interaction.rb b/lib/rubygems/user_interaction.rb index 4970f33c00..ffccb60314 100644 --- a/lib/rubygems/user_interaction.rb +++ b/lib/rubygems/user_interaction.rb @@ -68,7 +68,7 @@ module Gem include DefaultUserInteraction [ :choose_from_list, :ask, :ask_yes_no, :say, :alert, :alert_warning, - :alert_error, :terminate_interaction!, :terminate_interaction + :alert_error, :terminate_interaction ].each do |methname| class_eval %{ def #{methname}(*args) @@ -182,16 +182,10 @@ module Gem 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) + def terminate_interaction(status = 0) + raise Gem::SystemExitException, status end # Return a progress reporter object -- cgit v1.2.3