require 'rails/vendor_gem_source_index' module Gem def self.source_index=(index) @@source_index = index end end module Rails class GemDependency attr_accessor :lib, :source def self.unpacked_path @unpacked_path ||= File.join(RAILS_ROOT, 'vendor', 'gems') end @@framework_gems = {} def self.add_frozen_gem_path @@paths_loaded ||= begin source_index = Rails::VendorGemSourceIndex.new(Gem.source_index) Gem.clear_paths Gem.source_index = source_index # loaded before us - we can't change them, so mark them Gem.loaded_specs.each do |name, spec| @@framework_gems[name] = spec end true end end def framework_gem? @@framework_gems.has_key?(name) end def vendor_rails? Gem.loaded_specs.has_key?(name) && Gem.loaded_specs[name].loaded_from.empty? end def vendor_gem? Gem.loaded_specs.has_key?(name) && Gem.loaded_specs[name].loaded_from.include?(self.class.unpacked_path) end def initialize(name, options = {}) require 'rubygems' unless Object.const_defined?(:Gem) if options[:requirement] req = options[:requirement] elsif options[:version] req = Gem::Requirement.create(options[:version]) else req = Gem::Requirement.default end @dep = Gem::Dependency.new(name, req) @lib = options[:lib] @source = options[:source] @loaded = @frozen = @load_paths_added = false end def add_load_paths self.class.add_frozen_gem_path return if @loaded || @load_paths_added if framework_gem? @load_paths_added = @loaded = @frozen = true return end gem @dep @spec = Gem.loaded_specs[name] @frozen = @spec.loaded_from.include?(self.class.unpacked_path) if @spec @load_paths_added = true rescue Gem::LoadError end def dependencies(options = {}) return [] if framework_gem? || specification.nil? all_dependencies = specification.dependencies.map do |dependency| GemDependency.new(dependency.name, :requirement => dependency.version_requirements) end all_dependencies += all_dependencies.map { |d| d.dependencies(options) }.flatten if options[:flatten] all_dependencies.uniq end def gem_dir(base_directory) File.join(base_directory, specification.full_name) end def spec_filename(base_directory) File.join(gem_dir(base_directory), '.specification') end def load return if @loaded || @load_paths_added == false require(@lib || name) unless @lib == false @loaded = true rescue LoadError puts $!.to_s $!.backtrace.each { |b| puts b } end def name @dep.name.to_s end def requirement r = @dep.version_requirements (r == Gem::Requirement.default) ? nil : r end def frozen? @frozen ||= vendor_rails? || vendor_gem? end def loaded? @loaded ||= begin if vendor_rails? true elsif specification.nil? false else # check if the gem is loaded by inspecting $" # specification.files lists all the files contained in the gem gem_files = specification.files # select only the files contained in require_paths - typically in bin and lib require_paths_regexp = Regexp.new("^(#{specification.require_paths*'|'})/") gem_lib_files = gem_files.select { |f| require_paths_regexp.match(f) } # chop the leading directory off - a typical file might be in # lib/gem_name/file_name.rb, but it will be 'require'd as gem_name/file_name.rb gem_lib_files.map! { |f| f.split('/', 2)[1] } # if any of the files from the above list appear in $", the gem is assumed to # have been loaded !(gem_lib_files & $").empty? end end end def load_paths_added? # always try to add load paths - even if a gem is loaded, it may not # be a compatible version (ie random_gem 0.4 is loaded and a later spec # needs >= 0.5 - gem 'random_gem' will catch this and error out) @load_paths_added end def install cmd = "#{gem_command} #{install_command.join(' ')}" puts cmd puts %x(#{cmd}) end def unpack_to(directory) return if specification.nil? || File.directory?(gem_dir(directory)) || framework_gem? FileUtils.mkdir_p directory Dir.chdir directory do Gem::GemRunner.new.run(unpack_command) end # Gem.activate changes the spec - get the original real_spec = Gem::Specification.load(specification.loaded_from) write_spec(directory, real_spec) end def write_spec(directory, spec) # copy the gem's specification into GEMDIR/.specification so that # we can access information about the gem on deployment systems # without having the gem installed File.open(spec_filename(directory), 'w') do |file| file.puts spec.to_yaml end end def refresh_spec(directory) real_gems = Gem.source_index.installed_source_index exact_dep = Gem::Dependency.new(name, "= #{specification.version}") matches = real_gems.search(exact_dep) installed_spec = matches.first if File.exist?(File.dirname(spec_filename(directory))) if installed_spec # we have a real copy # get a fresh spec - matches should only have one element # note that there is no reliable method to check that the loaded # spec is the same as the copy from real_gems - Gem.activate changes # some of the fields real_spec = Gem::Specification.load(matches.first.loaded_from) write_spec(directory, real_spec) puts "Reloaded specification for #{name} from installed gems." else # the gem isn't installed locally - write out our current specs write_spec(directory, specification) puts "Gem #{name} not loaded locally - writing out current spec." end else if framework_gem? puts "Gem directory for #{name} not found - check if it's loading before rails." else puts "Something bad is going on - gem directory not found for #{name}." end end end def ==(other) self.name == other.name && self.requirement == other.requirement end alias_method :"eql?", :"==" def hash @dep.hash end def specification # code repeated from Gem.activate. Find a matching spec, or the currently loaded version. # error out if loaded version and requested version are incompatible. @spec ||= begin matches = Gem.source_index.search(@dep) matches << @@framework_gems[name] if framework_gem? if Gem.loaded_specs[name] then # This gem is already loaded. If the currently loaded gem is not in the # list of candidate gems, then we have a version conflict. existing_spec = Gem.loaded_specs[name] unless matches.any? { |spec| spec.version == existing_spec.version } then raise Gem::Exception, "can't activate #{@dep}, already activated #{existing_spec.full_name}" end # we're stuck with it, so change to match @dep.version_requirements = Gem::Requirement.create("=#{existing_spec.version}") existing_spec else # new load matches.last end end end private def gem_command RUBY_PLATFORM =~ /win32/ ? 'gem.bat' : 'gem' end def install_command cmd = %w(install) << name cmd << "--version" << %("#{requirement.to_s}") if requirement cmd << "--source" << @source if @source cmd end def unpack_command cmd = %w(unpack) << name cmd << "--version" << "= "+specification.version.to_s if requirement cmd end end end