diff options
author | yugui <yugui@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2008-08-25 15:02:05 +0000 |
---|---|---|
committer | yugui <yugui@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2008-08-25 15:02:05 +0000 |
commit | 0dc342de848a642ecce8db697b8fecd83a63e117 (patch) | |
tree | 2b7ed4724aff1f86073e4740134bda9c4aac1a39 /trunk/lib/rubygems/installer.rb | |
parent | ef70cf7138ab8034b5b806f466e4b484b24f0f88 (diff) |
added tag v1_9_0_4
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/tags/v1_9_0_4@18845 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'trunk/lib/rubygems/installer.rb')
-rw-r--r-- | trunk/lib/rubygems/installer.rb | 528 |
1 files changed, 528 insertions, 0 deletions
diff --git a/trunk/lib/rubygems/installer.rb b/trunk/lib/rubygems/installer.rb new file mode 100644 index 0000000000..5e2e8e0f42 --- /dev/null +++ b/trunk/lib/rubygems/installer.rb @@ -0,0 +1,528 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'fileutils' +require 'pathname' +require 'rbconfig' + +require 'rubygems/format' +require 'rubygems/ext' +require 'rubygems/require_paths_builder' + +## +# The installer class processes RubyGem .gem files and installs the +# files contained in the .gem into the Gem.path. +# +# Gem::Installer does the work of putting files in all the right places on the +# filesystem including unpacking the gem into its gem dir, installing the +# gemspec in the specifications dir, storing the cached gem in the cache dir, +# and installing either wrappers or symlinks for executables. +class Gem::Installer + + ## + # Raised when there is an error while building extensions. + # + class ExtensionBuildError < Gem::InstallError; end + + include Gem::UserInteraction + + include Gem::RequirePathsBuilder + + class << self + + attr_writer :exec_format + + # Defaults to use Ruby's program prefix and suffix. + def exec_format + @exec_format ||= Gem.default_exec_format + end + + end + + ## + # Constructs an Installer instance that will install the gem located at + # +gem+. +options+ is a Hash with the following keys: + # + # :env_shebang:: Use /usr/bin/env in bin wrappers. + # :force:: Overrides all version checks and security policy checks, except + # for a signed-gems-only policy. + # :ignore_dependencies:: Don't raise if a dependency is missing. + # :install_dir:: The directory to install the gem into. + # :format_executable:: Format the executable the same as the ruby executable. + # If your ruby is ruby18, foo_exec will be installed as + # foo_exec18. + # :security_policy:: Use the specified security policy. See Gem::Security + # :wrappers:: Install wrappers if true, symlinks if false. + + def initialize(gem, options={}) + @gem = gem + + options = { + :force => false, + :install_dir => Gem.dir, + :exec_format => false, + :env_shebang => false, + :bin_dir => nil + }.merge options + + @env_shebang = options[:env_shebang] + @force = options[:force] + gem_home = options[:install_dir] + @gem_home = Pathname.new(gem_home).expand_path + @ignore_dependencies = options[:ignore_dependencies] + @format_executable = options[:format_executable] + @security_policy = options[:security_policy] + @wrappers = options[:wrappers] + @bin_dir = options[:bin_dir] + @development = options[:development] + + begin + @format = Gem::Format.from_file_by_path @gem, @security_policy + rescue Gem::Package::FormatError + raise Gem::InstallError, "invalid gem format for #{@gem}" + end + + if not File.writable? @gem_home or + # TODO: Shouldn't have to test for existence of bindir; tests need it. + (@gem_home.to_s == Gem.dir and File.exist? Gem.bindir and + not File.writable? Gem.bindir) + if options[:user_install] == false # You explicitly don't want to use ~ + raise Gem::FilePermissionError, @gem_home + elsif options[:user_install].nil? + say "Warning: falling back to user-level install since #{@gem_home} and #{@bin_dir} aren't both writable." + end + options[:user_install] = true + end + + if options[:user_install] + @gem_home = File.join(ENV['HOME'], '.gem') + + user_bin_dir = File.join(@gem_home, 'gems', 'bin') + if !ENV['PATH'].split(':').include?(user_bin_dir) + say "You don't have #{user_bin_dir} in your PATH." + say "You won't be able to run gem-installed executables until you add it." + end + + Dir.mkdir @gem_home if ! File.directory? @gem_home + # If it's still not writable, you've got issues. + raise Gem::FilePermissionError, @gem_home if ! File.writable? @gem_home + end + + @spec = @format.spec + + @gem_dir = File.join(@gem_home, "gems", @spec.full_name).untaint + end + + ## + # Installs the gem and returns a loaded Gem::Specification for the installed + # gem. + # + # The gem will be installed with the following structure: + # + # @gem_home/ + # cache/<gem-version>.gem #=> a cached copy of the installed gem + # gems/<gem-version>/... #=> extracted files + # specifications/<gem-version>.gemspec #=> the Gem::Specification + + def install + # If we're forcing the install then disable security unless the security + # policy says that we only install singed gems. + @security_policy = nil if @force and @security_policy and + not @security_policy.only_signed + + unless @force then + if rrv = @spec.required_ruby_version then + unless rrv.satisfied_by? Gem.ruby_version then + raise Gem::InstallError, "#{@spec.name} requires Ruby version #{rrv}" + end + end + + if rrgv = @spec.required_rubygems_version then + unless rrgv.satisfied_by? Gem::Version.new(Gem::RubyGemsVersion) then + raise Gem::InstallError, + "#{@spec.name} requires RubyGems version #{rrgv}" + end + end + + unless @ignore_dependencies then + deps = @spec.runtime_dependencies + deps |= @spec.development_dependencies if @development + + deps.each do |dep_gem| + ensure_dependency @spec, dep_gem + end + end + end + + FileUtils.mkdir_p @gem_home unless File.directory? @gem_home + + Gem.ensure_gem_subdirectories @gem_home + + FileUtils.mkdir_p @gem_dir + + extract_files + generate_bin + build_extensions + write_spec + + write_require_paths_file_if_needed + + # HACK remove? Isn't this done in multiple places? + cached_gem = File.join @gem_home, "cache", @gem.split(/\//).pop + unless File.exist? cached_gem then + FileUtils.cp @gem, File.join(@gem_home, "cache") + end + + say @spec.post_install_message unless @spec.post_install_message.nil? + + @spec.loaded_from = File.join(@gem_home, 'specifications', + "#{@spec.full_name}.gemspec") + + Gem.source_index.add_spec @spec + + return @spec + rescue Zlib::GzipFile::Error + raise Gem::InstallError, "gzip error installing #{@gem}" + end + + ## + # Ensure that the dependency is satisfied by the current installation of + # gem. If it is not an exception is raised. + # + # spec :: Gem::Specification + # dependency :: Gem::Dependency + + def ensure_dependency(spec, dependency) + unless installation_satisfies_dependency? dependency then + raise Gem::InstallError, "#{spec.name} requires #{dependency}" + end + + true + end + + ## + # True if the gems in Gem.source_index satisfy +dependency+. + + def installation_satisfies_dependency?(dependency) + Gem.source_index.find_name(dependency.name, dependency.version_requirements).size > 0 + end + + ## + # Unpacks the gem into the given directory. + + def unpack(directory) + @gem_dir = directory + @format = Gem::Format.from_file_by_path @gem, @security_policy + extract_files + end + + ## + # Writes the .gemspec specification (in Ruby) to the supplied + # spec_path. + # + # spec:: [Gem::Specification] The Gem specification to output + # spec_path:: [String] The location (path) to write the gemspec to + + def write_spec + rubycode = @spec.to_ruby + + file_name = File.join @gem_home, 'specifications', + "#{@spec.full_name}.gemspec" + + file_name.untaint + + File.open(file_name, "w") do |file| + file.puts rubycode + end + end + + ## + # Creates windows .bat files for easy running of commands + + def generate_windows_script(bindir, filename) + if Gem.win_platform? then + script_name = filename + ".bat" + script_path = File.join bindir, File.basename(script_name) + File.open script_path, 'w' do |file| + file.puts windows_stub_script(bindir, filename) + end + + say script_path if Gem.configuration.really_verbose + end + end + + def generate_bin + return if @spec.executables.nil? or @spec.executables.empty? + + # If the user has asked for the gem to be installed in a directory that is + # the system gem directory, then use the system bin directory, else create + # (or use) a new bin dir under the gem_home. + bindir = @bin_dir ? @bin_dir : Gem.bindir(@gem_home) + + Dir.mkdir bindir unless File.exist? bindir + raise Gem::FilePermissionError.new(bindir) unless File.writable? bindir + + @spec.executables.each do |filename| + filename.untaint + bin_path = File.expand_path File.join(@gem_dir, @spec.bindir, filename) + mode = File.stat(bin_path).mode | 0111 + File.chmod mode, bin_path + + if @wrappers then + generate_bin_script filename, bindir + else + generate_bin_symlink filename, bindir + end + end + end + + ## + # Creates the scripts to run the applications in the gem. + #-- + # The Windows script is generated in addition to the regular one due to a + # bug or misfeature in the Windows shell's pipe. See + # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/193379 + + def generate_bin_script(filename, bindir) + bin_script_path = File.join bindir, formatted_program_filename(filename) + + exec_path = File.join @gem_dir, @spec.bindir, filename + + # HACK some gems don't have #! in their executables, restore 2008/06 + #if File.read(exec_path, 2) == '#!' then + FileUtils.rm_f bin_script_path # prior install may have been --no-wrappers + + File.open bin_script_path, 'w', 0755 do |file| + file.print app_script_text(filename) + end + + say bin_script_path if Gem.configuration.really_verbose + + generate_windows_script bindir, filename + #else + # FileUtils.rm_f bin_script_path + # FileUtils.cp exec_path, bin_script_path, + # :verbose => Gem.configuration.really_verbose + #end + end + + ## + # Creates the symlinks to run the applications in the gem. Moves + # the symlink if the gem being installed has a newer version. + + def generate_bin_symlink(filename, bindir) + if Gem.win_platform? then + alert_warning "Unable to use symlinks on Windows, installing wrapper" + generate_bin_script filename, bindir + return + end + + src = File.join @gem_dir, 'bin', filename + dst = File.join bindir, formatted_program_filename(filename) + + if File.exist? dst then + if File.symlink? dst then + link = File.readlink(dst).split File::SEPARATOR + cur_version = Gem::Version.create(link[-3].sub(/^.*-/, '')) + return if @spec.version < cur_version + end + File.unlink dst + end + + FileUtils.symlink src, dst, :verbose => Gem.configuration.really_verbose + end + + ## + # Generates a #! line for +bin_file_name+'s wrapper copying arguments if + # necessary. + + def shebang(bin_file_name) + if @env_shebang then + "#!/usr/bin/env " + Gem::ConfigMap[:ruby_install_name] + else + path = File.join @gem_dir, @spec.bindir, bin_file_name + + File.open(path, "rb") do |file| + first_line = file.gets + if first_line =~ /^#!/ then + # Preserve extra words on shebang line, like "-w". Thanks RPA. + shebang = first_line.sub(/\A\#!.*?ruby\S*/, "#!#{Gem.ruby}") + else + # Create a plain shebang line. + shebang = "#!#{Gem.ruby}" + end + + shebang.strip # Avoid nasty ^M issues. + end + end + end + + ## + # Return the text for an application file. + + def app_script_text(bin_file_name) + <<-TEXT +#{shebang bin_file_name} +# +# This file was generated by RubyGems. +# +# The application '#{@spec.name}' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'rubygems' + +version = "#{Gem::Requirement.default}" + +if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then + version = $1 + ARGV.shift +end + +gem '#{@spec.name}', version +load '#{bin_file_name}' +TEXT + end + + ## + # return the stub script text used to launch the true ruby script + + def windows_stub_script(bindir, bin_file_name) + <<-TEXT +@ECHO OFF +IF NOT "%~f0" == "~f0" GOTO :WinNT +@"#{File.basename(Gem.ruby)}" "#{File.join(bindir, bin_file_name)}" %1 %2 %3 %4 %5 %6 %7 %8 %9 +GOTO :EOF +:WinNT +@"#{File.basename(Gem.ruby)}" "%~dpn0" %* +TEXT + end + + ## + # Builds extensions. Valid types of extensions are extconf.rb files, + # configure scripts and rakefiles or mkrf_conf files. + + def build_extensions + return if @spec.extensions.empty? + say "Building native extensions. This could take a while..." + start_dir = Dir.pwd + dest_path = File.join @gem_dir, @spec.require_paths.first + ran_rake = false # only run rake once + + @spec.extensions.each do |extension| + break if ran_rake + results = [] + + builder = case extension + when /extconf/ then + Gem::Ext::ExtConfBuilder + when /configure/ then + Gem::Ext::ConfigureBuilder + when /rakefile/i, /mkrf_conf/i then + ran_rake = true + Gem::Ext::RakeBuilder + else + results = ["No builder for extension '#{extension}'"] + nil + end + + begin + Dir.chdir File.join(@gem_dir, File.dirname(extension)) + results = builder.build(extension, @gem_dir, dest_path, results) + + say results.join("\n") if Gem.configuration.really_verbose + + rescue => ex + results = results.join "\n" + + File.open('gem_make.out', 'wb') { |f| f.puts results } + + message = <<-EOF +ERROR: Failed to build gem native extension. + +#{results} + +Gem files will remain installed in #{@gem_dir} for inspection. +Results logged to #{File.join(Dir.pwd, 'gem_make.out')} + EOF + + raise ExtensionBuildError, message + ensure + Dir.chdir start_dir + end + end + end + + ## + # Reads the file index and extracts each file into the gem directory. + # + # Ensures that files can't be installed outside the gem directory. + + def extract_files + expand_and_validate_gem_dir + + raise ArgumentError, "format required to extract from" if @format.nil? + + @format.file_entries.each do |entry, file_data| + path = entry['path'].untaint + + if path =~ /\A\// then # for extra sanity + raise Gem::InstallError, + "attempt to install file into #{entry['path'].inspect}" + end + + path = File.expand_path File.join(@gem_dir, path) + + if path !~ /\A#{Regexp.escape @gem_dir}/ then + msg = "attempt to install file into %p under %p" % + [entry['path'], @gem_dir] + raise Gem::InstallError, msg + end + + FileUtils.mkdir_p File.dirname(path) + + File.open(path, "wb") do |out| + out.write file_data + end + + FileUtils.chmod entry['mode'], path + + say path if Gem.configuration.really_verbose + end + end + + ## + # Prefix and suffix the program filename the same as ruby. + + def formatted_program_filename(filename) + if @format_executable then + self.class.exec_format % File.basename(filename) + else + filename + end + end + + private + + ## + # HACK Pathname is broken on windows. + + def absolute_path? pathname + pathname.absolute? or (Gem.win_platform? and pathname.to_s =~ /\A[a-z]:/i) + end + + def expand_and_validate_gem_dir + @gem_dir = Pathname.new(@gem_dir).expand_path + + unless absolute_path?(@gem_dir) then # HACK is this possible after #expand_path? + raise ArgumentError, "install directory %p not absolute" % @gem_dir + end + + @gem_dir = @gem_dir.to_s + end + +end + |