From 0ba889680c504ff9660be4ef2af8c75cef402bf0 Mon Sep 17 00:00:00 2001 From: Matt Jones Date: Sun, 28 Sep 2008 02:08:12 -0400 Subject: [PATCH] Fix a number of errors in the config.gem mechanism. * Rails::GemDependency was missing definitions for hash and eql?, causing Array#uniq to not work. * If several versions of a gem were unpacked in vendor, selected the first (ie lowest version number) rather than the last (highest) version when no version was specified. * streamlined add_load_path. Now sets up Rubygems correctly to allow 'gem' to find frozen gems. * Rails::GemDependency#specification would return a spec for the highest installed version, even for frozen gems and/or previously loaded lower versions. See in part ticket #1123. * vendor/gems is now structured as a gem repository (has a 'gem' and a 'specifications' subdir). * removed vendor from default_load_paths - it was causing autoloading to append Gems::Gems:: to constant names * added additional tests for loading frozen gems. Outstanding issues: This patch does not address ticket #1107 (gems depending on vendor rails), nor does it address the rest of ticket #1123 (rake gems:unpack:dependencies will still unpack rails into vendor/gems). --- railties/lib/initializer.rb | 2 +- railties/lib/rails/gem_dependency.rb | 108 ++++++++++++-------- railties/lib/rails/plugin.rb | 2 +- railties/lib/tasks/gems.rake | 15 ++-- railties/test/gem_dependency_test.rb | 38 +++++++ .../gems/gems/dummy-gem-a-0.4.0/lib/dummy-gem-a.rb | 1 + .../gems/gems/dummy-gem-b-0.4.0/lib/dummy-gem-b.rb | 1 + .../gems/gems/dummy-gem-b-0.6.0/lib/dummy-gem-b.rb | 1 + .../gems/gems/dummy-gem-c-0.4.0/lib/dummy-gem-c.rb | 1 + .../gems/gems/dummy-gem-c-0.6.0/lib/dummy-gem-c.rb | 1 + .../gems/specifications/dummy-gem-a-0.4.0.gemspec | 10 ++ .../gems/specifications/dummy-gem-b-0.4.0.gemspec | 10 ++ .../gems/specifications/dummy-gem-b-0.6.0.gemspec | 10 ++ .../gems/specifications/dummy-gem-c-0.4.0.gemspec | 10 ++ .../gems/specifications/dummy-gem-c-0.6.0.gemspec | 10 ++ 15 files changed, 170 insertions(+), 50 deletions(-) create mode 100644 railties/test/vendor/gems/gems/dummy-gem-a-0.4.0/lib/dummy-gem-a.rb create mode 100644 railties/test/vendor/gems/gems/dummy-gem-b-0.4.0/lib/dummy-gem-b.rb create mode 100644 railties/test/vendor/gems/gems/dummy-gem-b-0.6.0/lib/dummy-gem-b.rb create mode 100644 railties/test/vendor/gems/gems/dummy-gem-c-0.4.0/lib/dummy-gem-c.rb create mode 100644 railties/test/vendor/gems/gems/dummy-gem-c-0.6.0/lib/dummy-gem-c.rb create mode 100644 railties/test/vendor/gems/specifications/dummy-gem-a-0.4.0.gemspec create mode 100644 railties/test/vendor/gems/specifications/dummy-gem-b-0.4.0.gemspec create mode 100644 railties/test/vendor/gems/specifications/dummy-gem-b-0.6.0.gemspec create mode 100644 railties/test/vendor/gems/specifications/dummy-gem-c-0.4.0.gemspec create mode 100644 railties/test/vendor/gems/specifications/dummy-gem-c-0.6.0.gemspec diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index 74d2daa..6431f73 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -264,6 +264,7 @@ module Rails end def add_gem_load_paths + Rails::GemDependency.add_frozen_gem_path unless @configuration.gems.empty? require "rubygems" @configuration.gems.each { |gem| gem.add_load_paths } @@ -878,7 +879,6 @@ Run `rake gems:install` to install the missing gems. components config lib - vendor ).map { |dir| "#{root_path}/#{dir}" }.select { |dir| File.directory?(dir) } paths.concat builtin_directories diff --git a/railties/lib/rails/gem_dependency.rb b/railties/lib/rails/gem_dependency.rb index d58ae45..ff1504f 100644 --- a/railties/lib/rails/gem_dependency.rb +++ b/railties/lib/rails/gem_dependency.rb @@ -1,45 +1,48 @@ module Rails class GemDependency - attr_accessor :name, :requirement, :version, :lib, :source + attr_accessor :lib, :source def self.unpacked_path - @unpacked_path ||= File.join(RAILS_ROOT, 'vendor', 'gems') + @unpacked_path ||= File.join(RAILS_ROOT, 'vendor', 'gems', 'gems') + end + + def self.specification_path + @specification_path ||= File.join(RAILS_ROOT, 'vendor', 'gems', 'specifications') + end + + def self.add_frozen_gem_path + frozen_gemspec_path = Rails::GemDependency.specification_path + if File.exist?(frozen_gemspec_path) && !Gem.source_index.spec_dirs.include?(frozen_gemspec_path) + Gem.source_index.spec_dirs.unshift frozen_gemspec_path + Gem.source_index.refresh! + Gem.path << Rails::GemDependency.unpacked_path + end end def initialize(name, options = {}) require 'rubygems' unless Object.const_defined?(:Gem) if options[:requirement] - @requirement = options[:requirement] + req = options[:requirement] elsif options[:version] - @requirement = Gem::Requirement.create(options[:version]) + req = Gem::Requirement.create(options[:version]) + else + req = Gem::Requirement.default end - @version = @requirement.instance_variable_get("@requirements").first.last if @requirement - @name = name.to_s + @dep = Gem::Dependency.new(name, req) @lib = options[:lib] @source = options[:source] @loaded = @frozen = @load_paths_added = false - @unpack_directory = nil - end - - def unpacked_paths - Dir[File.join(self.class.unpacked_path, "#{@name}-#{@version || "*"}")] end def add_load_paths return if @loaded || @load_paths_added - unpacked_paths = self.unpacked_paths - if unpacked_paths.empty? - args = [@name] - args << @requirement.to_s if @requirement - gem *args - else - $LOAD_PATH.unshift File.join(unpacked_paths.first, 'lib') - ext = File.join(unpacked_paths.first, 'ext') - $LOAD_PATH.unshift(ext) if File.exist?(ext) - @frozen = true - end + args = [name] + args << requirement.to_s if requirement + gem *args + @spec = Gem.loaded_specs[name] + @frozen = @spec.loaded_from.include?(self.class.specification_path) if @spec @load_paths_added = true rescue Gem::LoadError end @@ -52,19 +55,24 @@ module Rails all_dependencies.uniq end - def gem_dir(base_directory) - File.join(base_directory, specification.full_name) - end - def load return if @loaded || @load_paths_added == false - require(@lib || @name) unless @lib == 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 end @@ -84,26 +92,46 @@ module Rails end def unpack_to(directory) + FileUtils.mkdir_p self.class.specification_path FileUtils.mkdir_p directory Dir.chdir directory do Gem::GemRunner.new.run(unpack_command) end - # 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 - spec_filename = File.join(gem_dir(directory), '.specification') - File.open(spec_filename, 'w') do |file| - file.puts specification.to_yaml - end + # mirror the specification file into vendor/gems/specifications + FileUtils.cp specification.loaded_from, self.class.specification_path end def ==(other) self.name == other.name && self.requirement == other.requirement end + alias_method :"eql?", :"==" + + def hash + @dep.hash + end def specification - @spec ||= Gem.source_index.search(Gem::Dependency.new(@name, @requirement)).sort_by { |s| s.version }.last + # 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) + 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 #{gem}, 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 @@ -112,17 +140,15 @@ module Rails end def install_command - cmd = %w(install) << @name - cmd << "--version" << %("#{@requirement.to_s}") if @requirement + 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 - # We don't quote this requirement as it's run through GemRunner instead - # of shelling out to gem - cmd << "--version" << @requirement.to_s if @requirement + cmd = %w(unpack) << name + cmd << "--version" << requirement.to_s if requirement cmd end end diff --git a/railties/lib/rails/plugin.rb b/railties/lib/rails/plugin.rb index b8b2b57..4d98384 100644 --- a/railties/lib/rails/plugin.rb +++ b/railties/lib/rails/plugin.rb @@ -112,7 +112,7 @@ module Rails class GemPlugin < Plugin # Initialize this plugin from a Gem::Specification. def initialize(spec, gem) - directory = (gem.frozen? && gem.unpacked_paths.first) || File.join(spec.full_gem_path) + directory = spec.full_gem_path super(directory) @name = spec.name end diff --git a/railties/lib/tasks/gems.rake b/railties/lib/tasks/gems.rake index 0321e60..b6bd659 100644 --- a/railties/lib/tasks/gems.rake +++ b/railties/lib/tasks/gems.rake @@ -19,16 +19,17 @@ namespace :gems do task :build do $rails_gem_installer = true require 'rails/gem_builder' - Dir[File.join(RAILS_ROOT, 'vendor', 'gems', '*')].each do |gem_dir| - spec_file = File.join(gem_dir, '.specification') + Dir[File.join(Rails::GemDependency.unpacked_path, '*')].each do |gem_dir| + gem_name = File.basename(gem_dir) + spec_file = File.join(Rails::GemDependency.specification_path, "#{gem_name}.gemspec") next unless File.exists?(spec_file) - specification = YAML::load_file(spec_file) + specification = Gem::Specification.load(spec_file) next unless ENV['GEM'].blank? || ENV['GEM'] == specification.name Rails::GemBuilder.new(specification, gem_dir).build_extensions puts "Built gem: '#{gem_dir}'" end end - + desc "Installs all required gems for this application." task :install => :base do require 'rubygems' @@ -42,10 +43,10 @@ namespace :gems do require 'rubygems/gem_runner' Rails.configuration.gems.each do |gem| next unless !gem.frozen? && (ENV['GEM'].blank? || ENV['GEM'] == gem.name) - gem.unpack_to(File.join(RAILS_ROOT, 'vendor', 'gems')) if gem.loaded? + gem.unpack_to(Rails::GemDependency.unpacked_path) if gem.loaded? end end - + namespace :unpack do desc "Unpacks the specified gems and its dependencies into vendor/gems" task :dependencies => :unpack do @@ -56,7 +57,7 @@ namespace :gems do gem.dependencies.each do |dependency| dependency.add_load_paths # double check that we have not already unpacked next if dependency.frozen? - dependency.unpack_to(File.join(RAILS_ROOT, 'vendor', 'gems')) + dependency.unpack_to(Rails::GemDependency.unpacked_path) end end end diff --git a/railties/test/gem_dependency_test.rb b/railties/test/gem_dependency_test.rb index c94c950..6f1d588 100644 --- a/railties/test/gem_dependency_test.rb +++ b/railties/test/gem_dependency_test.rb @@ -71,5 +71,43 @@ uses_mocha "Plugin Tests" do @gem_without_load.load end + def test_gem_dependencies_compare_for_uniq + gem1 = Rails::GemDependency.new "gem1" + gem1a = Rails::GemDependency.new "gem1" + gem2 = Rails::GemDependency.new "gem2" + gem2a = Rails::GemDependency.new "gem2" + gem3 = Rails::GemDependency.new "gem2", :version => ">=0.1" + gem3a = Rails::GemDependency.new "gem2", :version => ">=0.1" + gem4 = Rails::GemDependency.new "gem3" + gems = [gem1, gem2, gem1a, gem3, gem2a, gem4, gem3a, gem2, gem4] + assert_equal 4, gems.uniq.size + end + + def test_gem_load_frozen + Rails::GemDependency.add_frozen_gem_path + dummy_gem = Rails::GemDependency.new "dummy-gem-a" + dummy_gem.add_load_paths + dummy_gem.load + assert_not_nil DUMMY_GEM_A_VERSION + end + + def test_gem_load_frozen_specific_version + Rails::GemDependency.add_frozen_gem_path + dummy_gem = Rails::GemDependency.new "dummy-gem-b", :version => '0.4.0' + dummy_gem.add_load_paths + dummy_gem.load + assert_not_nil DUMMY_GEM_B_VERSION + assert_equal '0.4.0', DUMMY_GEM_B_VERSION + end + + def test_gem_load_frozen_minimum_version + Rails::GemDependency.add_frozen_gem_path + dummy_gem = Rails::GemDependency.new "dummy-gem-c", :version => '>=0.5.0' + dummy_gem.add_load_paths + dummy_gem.load + assert_not_nil DUMMY_GEM_C_VERSION + assert_equal '0.6.0', DUMMY_GEM_C_VERSION + end + end end diff --git a/railties/test/vendor/gems/gems/dummy-gem-a-0.4.0/lib/dummy-gem-a.rb b/railties/test/vendor/gems/gems/dummy-gem-a-0.4.0/lib/dummy-gem-a.rb new file mode 100644 index 0000000..0453b38 --- /dev/null +++ b/railties/test/vendor/gems/gems/dummy-gem-a-0.4.0/lib/dummy-gem-a.rb @@ -0,0 +1 @@ +DUMMY_GEM_A_VERSION="0.4.0" diff --git a/railties/test/vendor/gems/gems/dummy-gem-b-0.4.0/lib/dummy-gem-b.rb b/railties/test/vendor/gems/gems/dummy-gem-b-0.4.0/lib/dummy-gem-b.rb new file mode 100644 index 0000000..850b5dd --- /dev/null +++ b/railties/test/vendor/gems/gems/dummy-gem-b-0.4.0/lib/dummy-gem-b.rb @@ -0,0 +1 @@ +DUMMY_GEM_B_VERSION="0.4.0" diff --git a/railties/test/vendor/gems/gems/dummy-gem-b-0.6.0/lib/dummy-gem-b.rb b/railties/test/vendor/gems/gems/dummy-gem-b-0.6.0/lib/dummy-gem-b.rb new file mode 100644 index 0000000..7d6d01c --- /dev/null +++ b/railties/test/vendor/gems/gems/dummy-gem-b-0.6.0/lib/dummy-gem-b.rb @@ -0,0 +1 @@ +DUMMY_GEM_B_VERSION="0.6.0" diff --git a/railties/test/vendor/gems/gems/dummy-gem-c-0.4.0/lib/dummy-gem-c.rb b/railties/test/vendor/gems/gems/dummy-gem-c-0.4.0/lib/dummy-gem-c.rb new file mode 100644 index 0000000..1a416be --- /dev/null +++ b/railties/test/vendor/gems/gems/dummy-gem-c-0.4.0/lib/dummy-gem-c.rb @@ -0,0 +1 @@ +DUMMY_GEM_C_VERSION="0.4.0" diff --git a/railties/test/vendor/gems/gems/dummy-gem-c-0.6.0/lib/dummy-gem-c.rb b/railties/test/vendor/gems/gems/dummy-gem-c-0.6.0/lib/dummy-gem-c.rb new file mode 100644 index 0000000..9ba2ca8 --- /dev/null +++ b/railties/test/vendor/gems/gems/dummy-gem-c-0.6.0/lib/dummy-gem-c.rb @@ -0,0 +1 @@ +DUMMY_GEM_C_VERSION="0.6.0" diff --git a/railties/test/vendor/gems/specifications/dummy-gem-a-0.4.0.gemspec b/railties/test/vendor/gems/specifications/dummy-gem-a-0.4.0.gemspec new file mode 100644 index 0000000..f791ed7 --- /dev/null +++ b/railties/test/vendor/gems/specifications/dummy-gem-a-0.4.0.gemspec @@ -0,0 +1,10 @@ +Gem::Specification.new do |s| + s.name = %q{dummy-gem-a} + s.version = "0.4.0" + s.date = %q{2008-09-28} + s.summary = "Dummy Gem A v0.4.0" + + s.files = ["lib/dummy-gem-a.rb"] + s.require_paths = ['lib'] + +end diff --git a/railties/test/vendor/gems/specifications/dummy-gem-b-0.4.0.gemspec b/railties/test/vendor/gems/specifications/dummy-gem-b-0.4.0.gemspec new file mode 100644 index 0000000..bb9f7db --- /dev/null +++ b/railties/test/vendor/gems/specifications/dummy-gem-b-0.4.0.gemspec @@ -0,0 +1,10 @@ +Gem::Specification.new do |s| + s.name = %q{dummy-gem-b} + s.version = "0.4.0" + s.date = %q{2008-09-28} + s.summary = "Dummy Gem B v0.4.0" + + s.files = ["lib/dummy-gem-b.rb"] + s.require_paths = ['lib'] + +end diff --git a/railties/test/vendor/gems/specifications/dummy-gem-b-0.6.0.gemspec b/railties/test/vendor/gems/specifications/dummy-gem-b-0.6.0.gemspec new file mode 100644 index 0000000..7f14a20 --- /dev/null +++ b/railties/test/vendor/gems/specifications/dummy-gem-b-0.6.0.gemspec @@ -0,0 +1,10 @@ +Gem::Specification.new do |s| + s.name = %q{dummy-gem-b} + s.version = "0.6.0" + s.date = %q{2008-09-28} + s.summary = "Dummy Gem B v0.6.0" + + s.files = ["lib/dummy-gem-b.rb"] + s.require_paths = ['lib'] + +end diff --git a/railties/test/vendor/gems/specifications/dummy-gem-c-0.4.0.gemspec b/railties/test/vendor/gems/specifications/dummy-gem-c-0.4.0.gemspec new file mode 100644 index 0000000..b504545 --- /dev/null +++ b/railties/test/vendor/gems/specifications/dummy-gem-c-0.4.0.gemspec @@ -0,0 +1,10 @@ +Gem::Specification.new do |s| + s.name = %q{dummy-gem-c} + s.version = "0.4.0" + s.date = %q{2008-09-28} + s.summary = "Dummy Gem C v0.4.0" + + s.files = ["lib/dummy-gem-c.rb"] + s.require_paths = ['lib'] + +end diff --git a/railties/test/vendor/gems/specifications/dummy-gem-c-0.6.0.gemspec b/railties/test/vendor/gems/specifications/dummy-gem-c-0.6.0.gemspec new file mode 100644 index 0000000..a45d9ca --- /dev/null +++ b/railties/test/vendor/gems/specifications/dummy-gem-c-0.6.0.gemspec @@ -0,0 +1,10 @@ +Gem::Specification.new do |s| + s.name = %q{dummy-gem-c} + s.version = "0.6.0" + s.date = %q{2008-09-28} + s.summary = "Dummy Gem C v0.6.0" + + s.files = ["lib/dummy-gem-c.rb"] + s.require_paths = ['lib'] + +end -- 1.5.3.1