From 16f6c5677d9c3979d006b4a4b393e7cf201a5651 Mon Sep 17 00:00:00 2001 From: Matt Jones Date: Sat, 11 Oct 2008 13:54:21 -0400 Subject: [PATCH] Make VendorGemSourceIndex handle broken/missing specs generated by previous versions. --- railties/lib/rails/gem_dependency.rb | 37 ++++++++- railties/lib/rails/vendor_gem_source_index.rb | 83 +++++++++++++++----- railties/lib/tasks/gems.rake | 10 +++ railties/test/gem_dependency_test.rb | 20 +++++ .../gems/dummy-gem-d-1.0.0/lib/dummy-gem-d.rb | 1 + .../vendor/gems/dummy-gem-e-1.0.0/.specification | 28 +++++++ .../gems/dummy-gem-e-1.0.0/lib/dummy-gem-e.rb | 1 + 7 files changed, 157 insertions(+), 23 deletions(-) create mode 100644 railties/test/vendor/gems/dummy-gem-d-1.0.0/lib/dummy-gem-d.rb create mode 100644 railties/test/vendor/gems/dummy-gem-e-1.0.0/.specification create mode 100644 railties/test/vendor/gems/dummy-gem-e-1.0.0/lib/dummy-gem-e.rb diff --git a/railties/lib/rails/gem_dependency.rb b/railties/lib/rails/gem_dependency.rb index ffdc403..fe573d3 100644 --- a/railties/lib/rails/gem_dependency.rb +++ b/railties/lib/rails/gem_dependency.rb @@ -82,6 +82,10 @@ module Rails 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 @@ -146,16 +150,41 @@ module Rails Gem::GemRunner.new.run(unpack_command) end + # Gem.activate changes the spec - get the original + real_spec = Gem::Specification.load(spec.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 - spec_filename = File.join(gem_dir(directory), '.specification') - # Gem.activate changes the spec - get the original - spec = Gem::Specification.load(specification.loaded_from) - File.open(spec_filename, 'w') do |file| + 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 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 + end def ==(other) self.name == other.name && self.requirement == other.requirement diff --git a/railties/lib/rails/vendor_gem_source_index.rb b/railties/lib/rails/vendor_gem_source_index.rb index e188f07..e8d76e7 100644 --- a/railties/lib/rails/vendor_gem_source_index.rb +++ b/railties/lib/rails/vendor_gem_source_index.rb @@ -13,6 +13,14 @@ module Rails attr_reader :installed_source_index attr_reader :vendor_source_index + def self.silence_spec_warnings + @@silence_spec_warnings + end + + def self.silence_spec_warnings=(v) + @@silence_spec_warnings = v + end + def initialize(installed_index, vendor_dir=Rails::GemDependency.unpacked_path) @installed_source_index = installed_index @vendor_dir = vendor_dir @@ -33,31 +41,68 @@ module Rails # load specifications from vendor/gems Dir[File.join(Rails::GemDependency.unpacked_path, '*')].each do |d| + dir_name = File.basename(d) + dir_version = version_for_dir(dir_name) spec = load_specification(d) - next unless spec - # NOTE: this is a bit of a hack - the gem system expects a different structure - # than we have. - # It's looking for: - # repository - # -> specifications - # - gem_name.spec <= loaded_from points to this - # -> gems - # - gem_name <= gem files here - # and therefore goes up one directory from loaded_from, then adds gems/gem_name - # to the path. - # But we have: - # vendor - # -> gems - # -> gem_name <= gem files here - # - .specification - # so we set loaded_from to vendor/gems/.specification (not a real file) to - # get the correct behavior. - spec.loaded_from = File.join(Rails::GemDependency.unpacked_path, '.specification') + if spec + if spec.full_name != dir_name + # mismatched directory name and gem spec - produced by 2.1.0-era unpack code + if dir_version + # fix the spec version - this is not optimal (spec.files may be wrong) + # but it's better than breaking apps. Complain to remind users to get correct specs. + # use ActiveSupport::Deprecation.warn, as the logger is not set yet + $stderr.puts("config.gem: Unpacked gem #{dir_name} in vendor/gems has a mismatched specification file."+ + " Run 'rake gems:refresh_specs' to fix this.") unless @@silence_spec_warnings + spec.version = dir_version + else + $stderr.puts("config.gem: Unpacked gem #{dir_name} in vendor/gems is not in a versioned directory"+ + "(should be #{spec.full_name}).") unless @@silence_spec_warnings + # continue, assume everything is OK + end + end + else + # no spec - produced by early-2008 unpack code + # emulate old behavior, and complain. + $stderr.puts("config.gem: Unpacked gem #{dir_name} in vendor/gems has no specification file."+ + " Run 'rake gems:refresh_specs' to fix this.") unless @@silence_spec_warnings + if dir_version + spec = Gem::Specification.new + spec.version = dir_version + spec.require_paths = ['lib'] + ext_path = File.join(d, 'ext') + spec.require_paths << 'ext' if File.exist?(ext_path) + spec.name = /^(.*)-[^-]+$/.match(dir_name)[1] + files = ['lib'] + # set files to everything in lib/ + files += Dir[File.join(d, 'lib', '*')].map { |v| v.gsub(/^#{d}\//, '') } + files += Dir[File.join(d, 'ext', '*')].map { |v| v.gsub(/^#{d}\//, '') } if ext_path + spec.files = files + else + $stderr.puts("config.gem: Unpacked gem #{dir_name} in vendor/gems not in a versioned directory."+ + " Giving up.") unless @silence_spec_warnings + next + end + end + spec.loaded_from = File.join(d, '.specification') + # finally, swap out full_gem_path + # it would be better to use a Gem::Specification subclass, but the YAML loads an explicit class + class << spec + def full_gem_path + path = File.join installation_path, full_name + return path if File.directory? path + File.join installation_path, original_name + end + end vendor_gems[File.basename(d)] = spec end @vendor_source_index = Gem::SourceIndex.new(vendor_gems) end + def version_for_dir(d) + matches = /-([^-]+)$/.match(d) + Gem::Version.new(matches[1]) if matches + end + def load_specification(gem_dir) spec_file = File.join(gem_dir, '.specification') YAML.load_file(spec_file) if File.exist?(spec_file) diff --git a/railties/lib/tasks/gems.rake b/railties/lib/tasks/gems.rake index 9abdfc5..c65dfc7 100644 --- a/railties/lib/tasks/gems.rake +++ b/railties/lib/tasks/gems.rake @@ -65,4 +65,14 @@ namespace :gems do end end end + + desc "Regenerate gem specifications in correct format." + task :refresh_specs => :base do + require 'rubygems' + require 'rubygems/gem_runner' + Rails.configuration.gems.each do |gem| + next unless gem.frozen? && (ENV['GEM'].blank? || ENV['GEM'] == gem.name) + gem.refresh_spec(Rails::GemDependency.unpacked_path) if gem.loaded? + end + end end \ No newline at end of file diff --git a/railties/test/gem_dependency_test.rb b/railties/test/gem_dependency_test.rb index 89e2534..5f026b2 100644 --- a/railties/test/gem_dependency_test.rb +++ b/railties/test/gem_dependency_test.rb @@ -1,3 +1,6 @@ +require 'lib/rails/vendor_gem_source_index' +Rails::VendorGemSourceIndex.silence_spec_warnings = true + require 'plugin_test_helper' class Rails::GemDependency @@ -110,5 +113,22 @@ uses_mocha "Plugin Tests" do assert_equal '0.6.0', DUMMY_GEM_C_VERSION end + def test_gem_load_missing_specification + dummy_gem = Rails::GemDependency.new "dummy-gem-d" + dummy_gem.add_load_paths + dummy_gem.load + assert_not_nil DUMMY_GEM_D_VERSION + assert_equal '1.0.0', DUMMY_GEM_D_VERSION + assert_equal ['lib', 'lib/dummy-gem-d.rb'], dummy_gem.specification.files + end + + def test_gem_load_bad_specification + dummy_gem = Rails::GemDependency.new "dummy-gem-e", :version => "= 1.0.0" + dummy_gem.add_load_paths + dummy_gem.load + assert_not_nil DUMMY_GEM_E_VERSION + assert_equal '1.0.0', DUMMY_GEM_E_VERSION + end + end end diff --git a/railties/test/vendor/gems/dummy-gem-d-1.0.0/lib/dummy-gem-d.rb b/railties/test/vendor/gems/dummy-gem-d-1.0.0/lib/dummy-gem-d.rb new file mode 100644 index 0000000..e5cb007 --- /dev/null +++ b/railties/test/vendor/gems/dummy-gem-d-1.0.0/lib/dummy-gem-d.rb @@ -0,0 +1 @@ +DUMMY_GEM_D_VERSION="1.0.0" diff --git a/railties/test/vendor/gems/dummy-gem-e-1.0.0/.specification b/railties/test/vendor/gems/dummy-gem-e-1.0.0/.specification new file mode 100644 index 0000000..ce4443c --- /dev/null +++ b/railties/test/vendor/gems/dummy-gem-e-1.0.0/.specification @@ -0,0 +1,28 @@ +--- !ruby/object:Gem::Specification +name: dummy-gem-e +version: !ruby/object:Gem::Version + version: 1.3.0 +platform: ruby +authors: +- "Nobody" +date: 2008-10-03 00:00:00 -04:00 +files: +- lib +- lib/dummy-gem-e.rb +require_paths: +- lib +required_ruby_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + version: +required_rubygems_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + version: +requirements: [] +specification_version: 2 +summary: Dummy Gem E diff --git a/railties/test/vendor/gems/dummy-gem-e-1.0.0/lib/dummy-gem-e.rb b/railties/test/vendor/gems/dummy-gem-e-1.0.0/lib/dummy-gem-e.rb new file mode 100644 index 0000000..48bf91a --- /dev/null +++ b/railties/test/vendor/gems/dummy-gem-e-1.0.0/lib/dummy-gem-e.rb @@ -0,0 +1 @@ +DUMMY_GEM_E_VERSION="1.0.0" -- 1.5.3.1