diff --git a/lib/rubygems.rb b/lib/rubygems.rb index d8f49d552b..08c675a86f 100644 --- a/lib/rubygems.rb +++ b/lib/rubygems.rb @@ -193,6 +193,7 @@ module Gem @ruby = nil @sources = [] + @post_build_hooks ||= [] @post_install_hooks ||= [] @post_uninstall_hooks ||= [] @pre_uninstall_hooks ||= [] @@ -728,6 +729,17 @@ module Gem @platforms end + ## + # Adds a post-build hook that will be passed an Gem::Installer instance + # when Gem::Installer#install is called. The hook is called after the gem + # has been extracted and extensions have been built but before the + # executables or gemspec has been written. If the hook returns +false+ then + # the gem's files will be removed and the install will be aborted. + + def self.post_build(&hook) + @post_build_hooks << hook + end + ## # Adds a post-install hook that will be passed an Gem::Installer instance # when Gem::Installer#install is called @@ -747,7 +759,8 @@ module Gem ## # Adds a pre-install hook that will be passed an Gem::Installer instance - # when Gem::Installer#install is called + # when Gem::Installer#install is called. If the hook returns +false+ then + # the install will be aborted. def self.pre_install(&hook) @pre_install_hooks << hook @@ -986,7 +999,8 @@ module Gem '.rb', *%w(DLEXT DLEXT2).map { |key| val = RbConfig::CONFIG[key] - ".#{val}" unless val.empty? + next unless val and not val.empty? + ".#{val}" } ].compact.uniq end @@ -1096,6 +1110,12 @@ module Gem attr_reader :loaded_specs + ## + # The list of hooks to be run before Gem::Install#install finishes + # installation + + attr_reader :post_build_hooks + ## # The list of hooks to be run before Gem::Install#install does any work @@ -1219,9 +1239,6 @@ end ## # Enables the require hook for RubyGems. -# -# Ruby 1.9 allows --disable-gems, so we require it when we didn't detect a Gem -# constant at rubygems.rb load time. require 'rubygems/custom_require' diff --git a/lib/rubygems/builder.rb b/lib/rubygems/builder.rb index a5f8fec352..b5d596b1f3 100644 --- a/lib/rubygems/builder.rb +++ b/lib/rubygems/builder.rb @@ -20,7 +20,6 @@ end Gem.load_yaml require 'rubygems/package' -require 'rubygems/security' ## # The Builder class processes RubyGem specification files @@ -73,6 +72,8 @@ EOM signer = nil if @spec.respond_to?(:signing_key) and @spec.signing_key then + require 'rubygems/security' + signer = Gem::Security::Signer.new @spec.signing_key, @spec.cert_chain @spec.signing_key = nil @spec.cert_chain = signer.cert_chain.map { |cert| cert.to_s } diff --git a/lib/rubygems/commands/sources_command.rb b/lib/rubygems/commands/sources_command.rb index ce483c0b5e..437fa574d0 100644 --- a/lib/rubygems/commands/sources_command.rb +++ b/lib/rubygems/commands/sources_command.rb @@ -57,12 +57,16 @@ class Gem::Commands::SourcesCommand < Gem::Command path = Gem::SpecFetcher.fetcher.dir FileUtils.rm_rf path - if not File.exist?(path) then + unless File.exist? path then say "*** Removed specs cache ***" - elsif not File.writable?(path) then - say "*** Unable to remove source cache (write protected) ***" else - say "*** Unable to remove source cache ***" + unless File.writable? path then + say "*** Unable to remove source cache (write protected) ***" + else + say "*** Unable to remove source cache ***" + end + + terminate_interaction 1 end end @@ -78,8 +82,10 @@ class Gem::Commands::SourcesCommand < Gem::Command say "#{source_uri} added to sources" rescue URI::Error, ArgumentError say "#{source_uri} is not a URI" + terminate_interaction 1 rescue Gem::RemoteFetcher::FetchError => e say "Error fetching #{source_uri}:\n\t#{e.message}" + terminate_interaction 1 end end diff --git a/lib/rubygems/install_update_options.rb b/lib/rubygems/install_update_options.rb index f3ec1aa12a..5073be0c21 100644 --- a/lib/rubygems/install_update_options.rb +++ b/lib/rubygems/install_update_options.rb @@ -11,7 +11,13 @@ #++ require 'rubygems' -require 'rubygems/security' + +# forward-declare + +module Gem::Security # :nodoc: + class Policy # :nodoc: + end +end ## # Mixin methods for install and update options for Gem::Commands @@ -23,8 +29,12 @@ module Gem::InstallUpdateOptions def add_install_update_options OptionParser.accept Gem::Security::Policy do |value| + require 'rubygems/security' + value = Gem::Security::Policies[value] - raise OptionParser::InvalidArgument, value if value.nil? + valid = Gem::Security::Policies.keys.sort + message = "#{value} (#{valid.join ', '} are valid)" + raise OptionParser::InvalidArgument, message if value.nil? value end diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 3665aa3e0b..7321d2e2e5 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -24,7 +24,7 @@ require 'rubygems/require_paths_builder' # gemspec in the specifications dir, storing the cached gem in the cache dir, # and installing either wrappers or symlinks for executables. # -# The installer fires pre and post install hooks. Hooks can be added either +# The installer invokes pre and post install hooks. Hooks can be added either # through a rubygems_plugin.rb file in an installed gem or via a # rubygems/defaults/#{RUBY_ENGINE}.rb or rubygems/defaults/operating_system.rb # file. See Gem.pre_install and Gem.post_install for details. @@ -61,6 +61,11 @@ class Gem::Installer attr_reader :spec + ## + # The options passed when the Gem::Installer was instantiated. + + attr_reader :options + @path_warning = false class << self @@ -98,49 +103,16 @@ class Gem::Installer require 'fileutils' @gem = gem - - options = { - :bin_dir => nil, - :env_shebang => false, - :exec_format => false, - :force => false, - :install_dir => Gem.dir, - :source_index => Gem.source_index, - }.merge options - - @env_shebang = options[:env_shebang] - @force = options[:force] - gem_home = options[:install_dir] - @gem_home = File.expand_path(gem_home) - @ignore_dependencies = options[:ignore_dependencies] - @format_executable = options[:format_executable] - @security_policy = options[:security_policy] - @wrappers = options[:wrappers] - @bin_dir = options[:bin_dir] - @development = options[:development] - @source_index = options[:source_index] - - begin - @format = Gem::Format.from_file_by_path @gem, @security_policy - rescue Gem::Package::FormatError - raise Gem::InstallError, "invalid gem format for #{@gem}" - end + @options = options + process_options + load_gem_file if options[:user_install] and not options[:unpack] then @gem_home = Gem.user_dir - - user_bin_dir = File.join(@gem_home, 'bin') - unless ENV['PATH'].split(File::PATH_SEPARATOR).include? user_bin_dir then - unless self.class.path_warning then - alert_warning "You don't have #{user_bin_dir} in your PATH,\n\t gem executables will not run." - self.class.path_warning = true - end - end + check_that_user_bin_dir_is_in_path end - FileUtils.mkdir_p @gem_home - raise Gem::FilePermissionError, @gem_home unless - options[:unpack] or File.writable? @gem_home + verify_gem_home(options[:unpack]) @spec = @format.spec @@ -160,48 +132,48 @@ class Gem::Installer def install # If we're forcing the install then disable security unless the security - # policy says that we only install singed gems. + # policy says that we only install signed gems. @security_policy = nil if @force and @security_policy and not @security_policy.only_signed - unless @force then - if rrv = @spec.required_ruby_version then - unless rrv.satisfied_by? Gem.ruby_version then - raise Gem::InstallError, "#{@spec.name} requires Ruby version #{rrv}." - end - end - - if rrgv = @spec.required_rubygems_version then - unless rrgv.satisfied_by? Gem::Version.new(Gem::VERSION) then - raise Gem::InstallError, - "#{@spec.name} requires RubyGems version #{rrgv}. " + - "Try 'gem update --system' to update RubyGems itself." - end - end - - unless @ignore_dependencies then - deps = @spec.runtime_dependencies - deps |= @spec.development_dependencies if @development - - deps.each do |dep_gem| - ensure_dependency @spec, dep_gem - end - end + unless @force + ensure_required_ruby_version_met + ensure_required_rubygems_version_met + ensure_dependencies_met unless @ignore_dependencies end Gem.pre_install_hooks.each do |hook| - hook.call self - end + result = hook.call self - FileUtils.mkdir_p @gem_home unless File.directory? @gem_home + if result == false then + location = " at #{$1}" if hook.inspect =~ /@(.*:\d+)/ + + message = "pre-install hook#{location} failed for #{@spec.full_name}" + raise Gem::InstallError, message + end + end Gem.ensure_gem_subdirectories @gem_home FileUtils.mkdir_p @gem_dir extract_files - generate_bin build_extensions + + Gem.post_build_hooks.each do |hook| + result = hook.call self + + if result == false then + FileUtils.rm_rf @gem_dir + + location = " at #{$1}" if hook.inspect =~ /@(.*:\d+)/ + + message = "post-build hook#{location} failed for #{@spec.full_name}" + raise Gem::InstallError, message + end + end + + generate_bin write_spec write_require_paths_file_if_needed if QUICKLOADER_SUCKAGE @@ -238,7 +210,6 @@ class Gem::Installer unless installation_satisfies_dependency? dependency then raise Gem::InstallError, "#{spec.name} requires #{dependency}" end - true end @@ -388,6 +359,80 @@ class Gem::Installer end end + def ensure_required_ruby_version_met + if rrv = @spec.required_ruby_version then + unless rrv.satisfied_by? Gem.ruby_version then + raise Gem::InstallError, "#{@spec.name} requires Ruby version #{rrv}." + end + end + end + + def ensure_required_rubygems_version_met + if rrgv = @spec.required_rubygems_version then + unless rrgv.satisfied_by? Gem::Version.new(Gem::VERSION) then + raise Gem::InstallError, + "#{@spec.name} requires RubyGems version #{rrgv}. " + + "Try 'gem update --system' to update RubyGems itself." + end + end + end + + def ensure_dependencies_met + deps = @spec.runtime_dependencies + deps |= @spec.development_dependencies if @development + + deps.each do |dep_gem| + ensure_dependency @spec, dep_gem + end + end + + def process_options + @options = { + :bin_dir => nil, + :env_shebang => false, + :exec_format => false, + :force => false, + :install_dir => Gem.dir, + :source_index => Gem.source_index, + }.merge @options + + @env_shebang = @options[:env_shebang] + @force = @options[:force] + gem_home = @options[:install_dir] + @gem_home = File.expand_path(gem_home) + @ignore_dependencies = @options[:ignore_dependencies] + @format_executable = @options[:format_executable] + @security_policy = @options[:security_policy] + @wrappers = @options[:wrappers] + @bin_dir = @options[:bin_dir] + @development = @options[:development] + @source_index = @options[:source_index] + end + + def load_gem_file + begin + @format = Gem::Format.from_file_by_path @gem, @security_policy + rescue Gem::Package::FormatError + raise Gem::InstallError, "invalid gem format for #{@gem}" + end + end + + def check_that_user_bin_dir_is_in_path + user_bin_dir = File.join(@gem_home, 'bin') + unless ENV['PATH'].split(File::PATH_SEPARATOR).include? user_bin_dir then + unless self.class.path_warning then + alert_warning "You don't have #{user_bin_dir} in your PATH,\n\t gem executables will not run." + self.class.path_warning = true + end + end + end + + def verify_gem_home(unpack = false) + FileUtils.mkdir_p @gem_home + raise Gem::FilePermissionError, @gem_home unless + unpack or File.writable? @gem_home + end + ## # Return the text for an application file. diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index a03263ed1a..0fd27a0108 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.rb @@ -10,7 +10,6 @@ # See LICENSE.txt for additional licensing information. #++ -require 'rubygems/security' require 'rubygems/specification' ## diff --git a/lib/rubygems/package/tar_output.rb b/lib/rubygems/package/tar_output.rb index f3be675e9f..bf785bc6d8 100644 --- a/lib/rubygems/package/tar_output.rb +++ b/lib/rubygems/package/tar_output.rb @@ -85,6 +85,7 @@ class Gem::Package::TarOutput # if we have a signing key, then sign the data # digest and return the signature if @signer then + require 'rubygems/security' digest = Gem::Security::OPT[:dgst_algo].digest sio.string @data_signature = @signer.sign digest inner.write sio.string @@ -113,6 +114,7 @@ class Gem::Package::TarOutput # if we have a signing key, then sign the metadata digest and return # the signature if @signer then + require 'rubygems/security' digest = Gem::Security::OPT[:dgst_algo].digest sio.string @meta_signature = @signer.sign digest io.write sio.string diff --git a/test/rubygems/gemutilities.rb b/test/rubygems/gemutilities.rb index fc27d6077c..9c94a128d0 100644 --- a/test/rubygems/gemutilities.rb +++ b/test/rubygems/gemutilities.rb @@ -130,11 +130,17 @@ class RubyGemTestCase < MiniTest::Unit::TestCase @public_cert = File.expand_path File.join(File.dirname(__FILE__), 'public_cert.pem') + Gem.post_build_hooks.clear Gem.post_install_hooks.clear Gem.post_uninstall_hooks.clear Gem.pre_install_hooks.clear Gem.pre_uninstall_hooks.clear + Gem.post_build do |installer| + @post_build_hook_arg = installer + true + end + Gem.post_install do |installer| @post_install_hook_arg = installer end @@ -145,6 +151,7 @@ class RubyGemTestCase < MiniTest::Unit::TestCase Gem.pre_install do |installer| @pre_install_hook_arg = installer + true end Gem.pre_uninstall do |uninstaller| diff --git a/test/rubygems/test_gem_commands_sources_command.rb b/test/rubygems/test_gem_commands_sources_command.rb index 718a0709dc..b747d08795 100644 --- a/test/rubygems/test_gem_commands_sources_command.rb +++ b/test/rubygems/test_gem_commands_sources_command.rb @@ -90,7 +90,9 @@ class TestGemCommandsSourcesCommand < RubyGemTestCase util_setup_spec_fetcher use_ui @ui do - @cmd.execute + assert_raises MockGemUi::TermError do + @cmd.execute + end end expected = <<-EOF @@ -108,7 +110,9 @@ Error fetching http://beta-gems.example.com: util_setup_spec_fetcher use_ui @ui do - @cmd.execute + assert_raises MockGemUi::TermError do + @cmd.execute + end end assert_equal [@gem_repo], Gem.sources diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index 3e329a2967..c77de792f3 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -521,15 +521,27 @@ load Gem.bin_path('a', 'my_exec', version) def test_install Dir.mkdir util_inst_bindir util_setup_gem + util_clear_gems + gemdir = File.join @gemhome, 'gems', @spec.full_name cache_file = File.join @gemhome, 'cache', @spec.file_name + stub_exe = File.join @gemhome, 'bin', 'executable' + rakefile = File.join gemdir, 'ext', 'a', 'Rakefile' Gem.pre_install do |installer| - refute File.exist?(cache_file), 'cache file should not exist yet' + refute File.exist?(cache_file), 'cache file must not exist yet' + true + end + + Gem.post_build do |installer| + assert File.exist?(gemdir), 'gem install dir must exist' + assert File.exist?(rakefile), 'gem executable must exist' + refute File.exist?(stub_exe), 'gem executable must not exist' + true end Gem.post_install do |installer| - assert File.exist?(cache_file), 'cache file should exist' + assert File.exist?(cache_file), 'cache file must exist' end build_rake_in do @@ -538,25 +550,27 @@ load Gem.bin_path('a', 'my_exec', version) end end - gemdir = File.join @gemhome, 'gems', @spec.full_name - assert File.exist?(gemdir) + assert File.exist? gemdir + assert File.exist?(stub_exe), 'gem executable must exist' + + exe = File.join gemdir, 'bin', 'executable' + assert File.exist? exe - exe = File.join(gemdir, 'bin', 'executable') - assert File.exist?(exe) exe_mode = File.stat(exe).mode & 0111 assert_equal 0111, exe_mode, "0%o" % exe_mode unless win_platform? assert File.exist?(File.join(gemdir, 'lib', 'code.rb')) - assert File.exist?(File.join(gemdir, 'ext', 'a', 'Rakefile')) + assert File.exist? rakefile spec_file = File.join(@gemhome, 'specifications', @spec.spec_name) assert_equal spec_file, @spec.loaded_from assert File.exist?(spec_file) - assert_same @installer, @pre_install_hook_arg + assert_same @installer, @post_build_hook_arg assert_same @installer, @post_install_hook_arg + assert_same @installer, @pre_install_hook_arg end def test_install_bad_gem @@ -669,6 +683,84 @@ load Gem.bin_path('a', 'my_exec', version) assert File.exist?(File.join(@gemhome, 'specifications', @spec.spec_name)) end + def test_install_post_build_false + util_clear_gems + + Gem.post_build do + false + end + + use_ui @ui do + e = assert_raises Gem::InstallError do + @installer.install + end + + location = "#{__FILE__}:#{__LINE__ - 9}" + + assert_equal "post-build hook at #{location} failed for a-2", e.message + end + + spec_file = File.join @gemhome, 'specifications', @spec.spec_name + refute File.exist? spec_file + + gem_dir = File.join @gemhome, 'gems', @spec.full_name + refute File.exist? gem_dir + end + + def test_install_post_build_nil + util_clear_gems + + Gem.post_build do + nil + end + + use_ui @ui do + @installer.install + end + + spec_file = File.join @gemhome, 'specifications', @spec.spec_name + assert File.exist? spec_file + + gem_dir = File.join @gemhome, 'gems', @spec.full_name + assert File.exist? gem_dir + end + + def test_install_pre_install_false + util_clear_gems + + Gem.pre_install do + false + end + + use_ui @ui do + e = assert_raises Gem::InstallError do + @installer.install + end + + location = "#{__FILE__}:#{__LINE__ - 9}" + + assert_equal "pre-install hook at #{location} failed for a-2", e.message + end + + spec_file = File.join @gemhome, 'specifications', @spec.spec_name + refute File.exist? spec_file + end + + def test_install_pre_install_nil + util_clear_gems + + Gem.pre_install do + nil + end + + use_ui @ui do + @installer.install + end + + spec_file = File.join @gemhome, 'specifications', @spec.spec_name + assert File.exist? spec_file + end + def test_install_with_message @spec.post_install_message = 'I am a shiny gem!'