From 0785b65f3ebfb14498acd84f4d0b9e4ee7419006 Mon Sep 17 00:00:00 2001 From: Carl Lerche Date: Thu, 30 Jul 2009 16:13:14 -0700 Subject: Starting to refactor the Bundler to get it ready for multiple kinds of sources --- lib/bundler.rb | 1 + lib/bundler/finder.rb | 59 +++++----------- lib/bundler/gem_bundle.rb | 9 +-- lib/bundler/gem_ext.rb | 4 +- lib/bundler/manifest.rb | 3 +- lib/bundler/manifest_file.rb | 2 +- lib/bundler/repository.rb | 8 +++ lib/bundler/runtime.rb | 6 +- lib/bundler/source.rb | 51 ++++++++++++++ spec/bundler/dsl_spec.rb | 125 --------------------------------- spec/bundler/fetcher_spec.rb | 94 +++++++++---------------- spec/bundler/finder_spec.rb | 11 ++- spec/bundler/gem_specification_spec.rb | 8 +-- spec/bundler/manifest_spec.rb | 5 -- 14 files changed, 138 insertions(+), 248 deletions(-) create mode 100644 lib/bundler/source.rb delete mode 100644 spec/bundler/dsl_spec.rb diff --git a/lib/bundler.rb b/lib/bundler.rb index a0f16896a1..fc7b51f749 100644 --- a/lib/bundler.rb +++ b/lib/bundler.rb @@ -7,6 +7,7 @@ require "rubygems/remote_fetcher" require "rubygems/installer" require "bundler/gem_bundle" +require "bundler/source" require "bundler/finder" require "bundler/gem_ext" require "bundler/resolver" diff --git a/lib/bundler/finder.rb b/lib/bundler/finder.rb index ab9d95146e..9c6e79431d 100644 --- a/lib/bundler/finder.rb +++ b/lib/bundler/finder.rb @@ -13,10 +13,9 @@ module Bundler # ==== Parameters # *sources:: URI pointing to the gem repository def initialize(*sources) - @results = {} - @index = Hash.new { |h,k| h[k] = {} } - - sources.each { |source| fetch(source) } + @cache = {} + @index = {} + @sources = sources end # Figures out the best possible configuration of gems that satisfies @@ -36,39 +35,6 @@ module Bundler resolved && GemBundle.new(resolved) end - # Fetches the index from the remote source - # - # ==== Parameters - # source:: URI pointing to the gem repository - # - # ==== Raises - # ArgumentError:: If the source is not a valid gem repository - def fetch(source) - Bundler.logger.info "Updating source: #{source}" - - deflated = Gem::RemoteFetcher.fetcher.fetch_path("#{source}/Marshal.4.8.Z") - inflated = Gem.inflate deflated - - append(Marshal.load(inflated), source) - rescue Gem::RemoteFetcher::FetchError => e - raise ArgumentError, "#{source} is not a valid source: #{e.message}" - end - - # Adds a new gem index linked to a gem source to the over all - # gem index that gets searched. - # - # ==== Parameters - # index:: The index to append to the list - # source:: The original source - def append(index, source) - index.gems.values.each do |spec| - next unless Gem::Platform.match(spec.platform) - spec.source = source - @index[spec.name][spec.version] ||= spec - end - self - end - # Searches for a gem that matches the dependency # # ==== Parameters @@ -78,12 +44,25 @@ module Bundler # [Gem::Specification]:: A collection of gem specifications # matching the search def search(dependency) - @results[dependency.hash] ||= begin - possibilities = @index[dependency.name].values - possibilities.select do |spec| + @cache[dependency.hash] ||= begin + find_by_name(dependency.name).select do |spec| dependency =~ spec end.sort_by {|s| s.version } end end + + private + + def find_by_name(name) + matches = @index[name] ||= begin + versions = {} + @sources.reverse_each do |source| + versions.merge! source.specs[name] || {} + end + versions + end + matches.values + end + end end \ No newline at end of file diff --git a/lib/bundler/gem_bundle.rb b/lib/bundler/gem_bundle.rb index b2dc015875..8c5cad558d 100644 --- a/lib/bundler/gem_bundle.rb +++ b/lib/bundler/gem_bundle.rb @@ -1,13 +1,8 @@ module Bundler class GemBundle < Array - def download(directory) - FileUtils.mkdir_p(directory) - + def download(repository) sort_by {|s| s.full_name.downcase }.each do |spec| - unless directory.join("cache", "#{spec.full_name}.gem").file? - Bundler.logger.info "Downloading #{spec.full_name}.gem" - Gem::RemoteFetcher.fetcher.download(spec, spec.source, directory) - end + repository.download(spec) end self diff --git a/lib/bundler/gem_ext.rb b/lib/bundler/gem_ext.rb index 8a38322326..15f23e6554 100644 --- a/lib/bundler/gem_ext.rb +++ b/lib/bundler/gem_ext.rb @@ -12,8 +12,8 @@ module Gem attribute :source def source=(source) - @source = source.is_a?(URI) ? source : URI.parse(source) - raise ArgumentError, "The source must be an absolute URI" unless @source.absolute? + source = Bundler::Source.new(source) unless source.is_a?(Bundler::Source) + @source = source end end end diff --git a/lib/bundler/manifest.rb b/lib/bundler/manifest.rb index 690e419155..ca05d46223 100644 --- a/lib/bundler/manifest.rb +++ b/lib/bundler/manifest.rb @@ -7,7 +7,6 @@ module Bundler attr_reader :sources, :dependencies, :path def initialize(sources, dependencies, bindir, repository_path, rubygems, system_gems) - sources.map! {|s| s.is_a?(URI) ? s : URI.parse(s) } @sources = sources @dependencies = dependencies @bindir = bindir @@ -58,7 +57,7 @@ module Bundler raise VersionConflict, "No compatible versions could be found for:\n#{gems}" end - bundle.download(@repository.path) + bundle.download(@repository) end def gem_dependencies diff --git a/lib/bundler/manifest_file.rb b/lib/bundler/manifest_file.rb index c766bca85a..2cad47f63e 100644 --- a/lib/bundler/manifest_file.rb +++ b/lib/bundler/manifest_file.rb @@ -11,7 +11,7 @@ module Bundler def initialize(filename) @filename = filename - @sources = %w(http://gems.rubyforge.org) + @sources = [Source.new("http://gems.rubyforge.org")] @dependencies = [] @rubygems = true @system_gems = true diff --git a/lib/bundler/repository.rb b/lib/bundler/repository.rb index 5c554edc9c..1ca53f43ab 100644 --- a/lib/bundler/repository.rb +++ b/lib/bundler/repository.rb @@ -22,6 +22,14 @@ module Bundler (Dir[@path.join("*")] - Dir[@path.join("{cache,doc,gems,environments,specifications}")]).empty? end + def download(spec) + FileUtils.mkdir_p(@path) + + unless @path.join("cache", "#{spec.full_name}.gem").file? + spec.source.download(spec, @path) + end + end + # Checks whether a gem is installed def install_cached_gems(options = {}) cached_gems.each do |name, version| diff --git a/lib/bundler/runtime.rb b/lib/bundler/runtime.rb index eadfe00b6b..35c600d35a 100644 --- a/lib/bundler/runtime.rb +++ b/lib/bundler/runtime.rb @@ -41,8 +41,10 @@ module Bundler end def source(source) - @manifest_file.sources << source - @manifest_file.sources.uniq! + source = Source.new(source) + unless @manifest_file.sources.include?(source) + @manifest_file.sources << source + end end def sources diff --git a/lib/bundler/source.rb b/lib/bundler/source.rb new file mode 100644 index 0000000000..dd1a1c5e7d --- /dev/null +++ b/lib/bundler/source.rb @@ -0,0 +1,51 @@ +module Bundler + # Represents a source of rubygems. Initially, this is only gem repositories, but + # eventually, this will be git, svn, HTTP + class Source + attr_reader :uri + + def initialize(uri) + @uri = uri.is_a?(URI) ? uri : URI.parse(uri) + raise ArgumentError, "The source must be an absolute URI" unless @uri.absolute? + end + + def specs + @specs ||= fetch_specs + end + + def ==(other) + uri == other.uri + end + + def to_s + @uri.to_s + end + + def download(spec, destination) + Bundler.logger.info "Downloading #{spec.full_name}.gem" + Gem::RemoteFetcher.fetcher.download(spec, uri, destination) + end + + private + + def fetch_specs + Bundler.logger.info "Updating source: #{to_s}" + + deflated = Gem::RemoteFetcher.fetcher.fetch_path("#{uri}/Marshal.4.8.Z") + inflated = Gem.inflate deflated + + index = Marshal.load(inflated) + specs = Hash.new { |h,k| h[k] = {} } + + index.gems.values.each do |spec| + next unless Gem::Platform.match(spec.platform) + spec.source = self + specs[spec.name][spec.version] = spec + end + + specs + rescue Gem::RemoteFetcher::FetchError => e + raise ArgumentError, "#{to_s} is not a valid source: #{e.message}" + end + end +end \ No newline at end of file diff --git a/spec/bundler/dsl_spec.rb b/spec/bundler/dsl_spec.rb deleted file mode 100644 index fe9fa847b7..0000000000 --- a/spec/bundler/dsl_spec.rb +++ /dev/null @@ -1,125 +0,0 @@ -require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') - -describe "Bundling DSL" do - - it "allows specifying the path to bundle gems to" do - build_manifest.gem_path.should == tmp_file("vendor", "gems") - end - - it "allows specifying sources" do - manifest = build_manifest <<-DSL - source "http://gems.github.com" - DSL - - manifest.sources.first.should == URI.parse("http://gems.rubyforge.org") - manifest.sources.last.should == URI.parse("http://gems.github.com") - end - - it "allows specifying gems" do - manifest = build_manifest <<-DSL - gem "rails" - DSL - - manifest.dependencies.first.name.should == "rails" - end - - it "allows specifying gem versions" do - manifest = build_manifest <<-DSL - gem "rails", ">= 2.0.0" - DSL - - manifest.dependencies.first.version.should == ">= 2.0.0" - end - - it "allows specifying how to require the gem" do - manifest = build_manifest <<-DSL - gem "actionpack", :require_as => "action_controller" - DSL - - manifest.dependencies.first.require_as.should == ["action_controller"] - end - - it "allows specifying 'only' restrictions on the environment" do - manifest = build_manifest <<-DSL - gem "ruby-debug", :only => "development" - DSL - - manifest.dependencies.first.only.should == ["development"] - end - - it "allows specifying 'except' restrictions on the environment" do - manifest = build_manifest <<-DSL - gem "newrelic_rpm", :except => "staging" - DSL - - manifest.dependencies.first.except.should == ["staging"] - end - - it "loads the manifest from a file" do - manifest = build_manifest(tmp_file("manifest.rb"), <<-DSL) - gem "rails" - DSL - - manifest.dependencies.first.name.should == "rails" - end - - it "allows specifying an arbitrary number of sources and gems" do - manifest = build_manifest <<-DSL - gem "thor" - source "http://gems.github.com" - gem "wycats-merb-core" - gem "mislav-will_paginate" - source "http://gems.example.org" - gem "uuidtools" - DSL - - manifest.sources.should == [ - URI.parse("http://gems.rubyforge.org"), - URI.parse("http://gems.github.com"), - URI.parse("http://gems.example.org") - ] - - manifest.dependencies.map { |d| d.name }.should == %w( - thor - wycats-merb-core - mislav-will_paginate - uuidtools - ) - end - - it "can bundle gems in a manifest defined through the DSL" do - manifest = build_manifest <<-DSL - sources.clear - - source "file://#{gem_repo1}" - source "file://#{gem_repo2}" - gem "merb-core", "= 1.0.12" - gem "activerecord", "> 2.2" - DSL - - gems = %w( - abstract-1.0.0 activerecord-2.3.2 activesupport-2.3.2 erubis-2.6.4 - extlib-0.9.12 json_pure-1.1.7 merb-core-1.0.12 mime-types-1.16 - rack-1.0.0 rake-0.8.7 rspec-1.2.8 thor-0.9.9) - - manifest.install - - tmp_gem_path.should have_cached_gems(*gems) - tmp_gem_path.should have_installed_gems(*gems) - - load_paths = {} - gems.each { |g| load_paths[g] = %w(bin lib) } - - tmp_gem_path('environments', 'default.rb').should have_load_paths(tmp_gem_path, load_paths) - end - - it "outputs a pretty error when an environment is named rubygems" do - lambda do - build_manifest <<-DSL - sources.clear - - gem "extlib", :only => "rubygems" - DSL - end.should raise_error(Bundler::InvalidEnvironmentName) - end -end diff --git a/spec/bundler/fetcher_spec.rb b/spec/bundler/fetcher_spec.rb index 67d3216e0e..67c9571821 100644 --- a/spec/bundler/fetcher_spec.rb +++ b/spec/bundler/fetcher_spec.rb @@ -2,8 +2,8 @@ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe "Fetcher" do before(:each) do - @source = URI.parse("file://#{gem_repo1}") - @other = URI.parse("file://#{gem_repo2}") + @source = Bundler::Source.new("file://#{gem_repo1}") + @other = Bundler::Source.new("file://#{gem_repo2}") @finder = Bundler::Finder.new(@source, @other) end @@ -15,10 +15,31 @@ describe "Fetcher" do @finder.search(build_dep("activerecord", "= 2.3.2")).first.source.should == @source end - it "raises if the source is invalid" do - lambda { Bundler::Finder.new.fetch("file://not/a/gem/source") }.should raise_error(ArgumentError) - lambda { Bundler::Finder.new.fetch("http://localhost") }.should raise_error(ArgumentError) - lambda { Bundler::Finder.new.fetch("http://google.com/not/a/gem/location") }.should raise_error(ArgumentError) + it "raises if the source does not exist" do + m = build_manifest <<-Gemfile + sources.clear + source "file://not/a/gem/source" + gem "foo" + Gemfile + lambda { m.install }.should raise_error(ArgumentError) + end + + it "raises if the source is not available" do + m = build_manifest <<-Gemfile + sources.clear + source "http://localhost" + gem "foo" + Gemfile + lambda { m.install }.should raise_error(ArgumentError) + end + + it "raises if the source is not a gem repository" do + m = build_manifest <<-Gemfile + sources.clear + source "http://google.com/not/a/gem/location" + gem "foo" + Gemfile + lambda { m.install }.should raise_error(ArgumentError) end it "accepts multiple source indexes" do @@ -34,7 +55,7 @@ describe "Fetcher" do # Try out windows Gem.platforms = [Gem::Platform.new("mswin32_60")] - finder = Bundler::Finder.new(@source) + finder = Bundler::Finder.new(Bundler::Source.new("file://#{gem_repo1}")) finder.search(build_dep("do_sqlite3", "> 0")).should only_have_specs("do_sqlite3-0.9.12-x86-mswin32-60") ensure Gem.platforms = nil @@ -42,60 +63,15 @@ describe "Fetcher" do end it "outputs a logger message when updating an index from source" do + m = build_manifest <<-Gemfile + sources.clear + source "file://#{gem_repo1}" + source "file://#{gem_repo2}" + gem "very-simple" + Gemfile + m.install @log_output.should have_log_message("Updating source: file:#{gem_repo1}") @log_output.should have_log_message("Updating source: file:#{gem_repo2}") end - describe "resolving rails" do - before(:each) do - @bundle = @finder.resolve(build_dep('rails', '>= 0')) - - FileUtils.mkdir_p(File.join(tmp_dir, 'cache')) - Dir[File.join(tmp_dir, 'cache', '*')].each { |file| FileUtils.rm_f(file) } - end - - it "resolves rails" do - @bundle.should match_gems( - "rails" => ["2.3.2"], - "actionpack" => ["2.3.2"], - "actionmailer" => ["2.3.2"], - "activerecord" => ["2.3.2"], - "activeresource" => ["2.3.2"], - "activesupport" => ["2.3.2"], - "rake" => ["0.8.7"] - ) - end - - it "keeps track of the source that the gem spec was found in" do - @bundle.select { |spec| spec.name == "activeresource" && spec.source == @other }.should have(1).item - @bundle.select { |spec| spec.name == "activerecord" && spec.source == @source }.should have(1).item - end - - it "can download the bundle" do - @bundle.download(tmp_dir) - @bundle.should be_cached_at(tmp_dir) - end - - it "does not download the gem if the gem is the same as the cached version" do - copy("actionmailer-2.3.2") - - lambda { - @bundle.download(tmp_dir) - @bundle.should be_cached_at(tmp_dir) - }.should_not change { File.mtime(fixture("actionmailer-2.3.2")) } - end - - it "outputs a logger message when resolving dependencies" do - @log_output.should have_log_message("Calculating dependencies...") - end - - it "outputs a logger message for each gem it downloads" do - @bundle.download(tmp_dir) - - %w(rails-2.3.2 actionpack-2.3.2 actionmailer-2.3.2 activerecord-2.3.2 - activeresource-2.3.2 activesupport-2.3.2 rake-0.8.7).each do |name| - @log_output.should have_log_message("Downloading #{name}.gem") - end - end - end end \ No newline at end of file diff --git a/spec/bundler/finder_spec.rb b/spec/bundler/finder_spec.rb index d95e8d1ebe..5bf3d4e753 100644 --- a/spec/bundler/finder_spec.rb +++ b/spec/bundler/finder_spec.rb @@ -1,6 +1,7 @@ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe "Finder" do + # TODO: Refactor all of these specs before(:each) do index = build_index do add_spec "activemerchant", "1.4.1" do @@ -13,7 +14,15 @@ describe "Finder" do end end - @faster = Bundler::Finder.new.append(index, "http://foo") + def index.specs + specs = Hash.new{|h,k| h[k] = {}} + @gems.values.each do |spec| + specs[spec.name][spec.version] = spec + end + specs + end + + @faster = Bundler::Finder.new(index) end it "find the gem given correct search" do diff --git a/spec/bundler/gem_specification_spec.rb b/spec/bundler/gem_specification_spec.rb index f98182d870..3389196efc 100644 --- a/spec/bundler/gem_specification_spec.rb +++ b/spec/bundler/gem_specification_spec.rb @@ -9,7 +9,7 @@ describe "Gem::Specification" do s.source = 'http://gems.rubyforge.org' end - spec.source.should == URI.parse("http://gems.rubyforge.org") + spec.source.should == Bundler::Source.new("http://gems.rubyforge.org") end it "does not consider two gem specs with different sources to be the same" do @@ -20,13 +20,13 @@ describe "Gem::Specification" do end spec2 = spec1.dup - spec2.source = "http://gems.github.com" + spec2.source = Bundler::Source.new("http://gems.github.com") spec1.should_not == spec2 end - it "can set a source that is already a URI" do - source = URI.parse("http://foo") + it "can set a source that is already a Source" do + source = Bundler::Source.new("http://foo") spec = Gem::Specification.new spec.source = source spec.source.should == source diff --git a/spec/bundler/manifest_spec.rb b/spec/bundler/manifest_spec.rb index b843e5262e..68fbfa9342 100644 --- a/spec/bundler/manifest_spec.rb +++ b/spec/bundler/manifest_spec.rb @@ -32,11 +32,6 @@ describe "Bundler::Manifest" do $".replace @saved_loaded_features end - it "has a list of sources and dependencies" do - @manifest.sources.should == @sources.map { |s| URI.parse(s) } - @manifest.dependencies.should == @deps - end - it "bundles itself (running all of the steps)" do @manifest.install -- cgit v1.2.1