lib/bundler/source.rb in bundler-0.8.1 vs lib/bundler/source.rb in bundler-0.9.0.pre1

- old
+ new

@@ -1,357 +1,247 @@ +require "rubygems/remote_fetcher" +require "rubygems/format" +require "digest/sha1" + module Bundler - class DirectorySourceError < StandardError; end - class GitSourceError < StandardError ; end - # Represents a source of rubygems. Initially, this is only gem repositories, but - # eventually, this will be git, svn, HTTP - class Source - attr_reader :bundle + module Source + class Rubygems + attr_reader :uri, :options - def initialize(bundle, options) - @bundle = bundle - end + def initialize(options = {}) + @options = options + @uri = options[:uri] + @uri = URI.parse(@uri) unless @uri.is_a?(URI) + raise ArgumentError, "The source must be an absolute URI" unless @uri.absolute? + end - private - - def process_source_gems(gems) - new_gems = Hash.new { |h,k| h[k] = [] } - gems.values.each do |spec| - spec.source = self - new_gems[spec.name] << spec + def specs + @specs ||= fetch_specs end - new_gems - end - end - class GemSource < Source - attr_reader :uri + def install(spec) + Bundler.ui.info "* #{spec.name} (#{spec.version})" + if Index.from_installed_gems[spec].any? + Bundler.ui.info " * already installed... skipping" + return + end - def initialize(bundle, options) - super - @uri = options[:uri] - @uri = URI.parse(@uri) unless @uri.is_a?(URI) - raise ArgumentError, "The source must be an absolute URI" unless @uri.absolute? - end + destination = Gem.dir - def local? - false - end + Bundler.ui.info " * Downloading..." + gem_path = Gem::RemoteFetcher.fetcher.download(spec, uri, destination) + Bundler.ui.info " * Installing..." + installer = Gem::Installer.new gem_path, + :install_dir => Gem.dir, + :ignore_dependencies => true - def gems - @specs ||= fetch_specs - end + installer.install + end - def ==(other) - uri == other.uri - end + private - def to_s - @uri.to_s - end + def fetch_specs + index = Index.new + Bundler.ui.info "Source: Fetching remote index for `#{uri}`... " + (main_specs + prerelease_specs).each do |name, version, platform| + spec = RemoteSpecification.new(name, version, platform, @uri) + spec.source = self + index << spec + end + Bundler.ui.info "done." + index.freeze + end - class RubygemsRetardation < StandardError; end - - def download(spec) - Bundler.logger.info "Downloading #{spec.full_name}.gem" - - destination = bundle.gem_path - - unless destination.writable? - raise RubygemsRetardation, "destination: #{destination} is not writable" + def main_specs + Marshal.load(Gem::RemoteFetcher.fetcher.fetch_path("#{uri}/specs.4.8.gz")) + rescue Gem::RemoteFetcher::FetchError => e + raise ArgumentError, "#{to_s} is not a valid source: #{e.message}" end - # Download the gem - Gem::RemoteFetcher.fetcher.download(spec, uri, destination) - - # Re-read the gemspec from the downloaded gem to correct - # any errors that were present in the Rubyforge specification. - new_spec = Gem::Format.from_file_by_path(destination.join('cache', "#{spec.full_name}.gem")).spec - spec.__swap__(new_spec) + def prerelease_specs + Marshal.load(Gem::RemoteFetcher.fetcher.fetch_path("#{uri}/prerelease_specs.4.8.gz")) + rescue Gem::RemoteFetcher::FetchError + Bundler.logger.warn "Source '#{uri}' does not support prerelease gems" + [] + end end - private - - def fetch_specs - Bundler.logger.info "Updating source: #{to_s}" - build_gem_index(fetch_main_specs + fetch_prerelease_specs) - end - - def build_gem_index(index) - gems = Hash.new { |h,k| h[k] = [] } - index.each do |name, version, platform| - spec = RemoteSpecification.new(name, version, platform, @uri) - spec.source = self - gems[spec.name] << spec if Gem::Platform.match(spec.platform) + class GemCache + def initialize(options) + @path = options[:path] end - gems - end - def fetch_main_specs - Marshal.load(Gem::RemoteFetcher.fetcher.fetch_path("#{uri}/specs.4.8.gz")) - rescue Gem::RemoteFetcher::FetchError => e - raise ArgumentError, "#{to_s} is not a valid source: #{e.message}" - end + def specs + @specs ||= begin + index = Index.new - def fetch_prerelease_specs - Marshal.load(Gem::RemoteFetcher.fetcher.fetch_path("#{uri}/prerelease_specs.4.8.gz")) - rescue Gem::RemoteFetcher::FetchError - Bundler.logger.warn "Source '#{uri}' does not support prerelease gems" - [] - end - end + Dir["#{@path}/*.gem"].each do |gemfile| + spec = Gem::Format.from_file_by_path(gemfile).spec + spec.source = self + index << spec + end - class SystemGemSource < Source + index.freeze + end + end - def self.instance - @instance - end + def install(spec) + destination = Gem.dir - def self.new(*args) - @instance ||= super - end + installer = Gem::Installer.new "#{@path}/#{spec.full_name}.gem", + :install_dir => Gem.dir, + :ignore_dependencies => true - def initialize(bundle, options = {}) - super - @source = Gem::SourceIndex.from_installed_gems + installer.install + end end - def local? - false - end + class Path + attr_reader :path, :options - def gems - @gems ||= process_source_gems(@source.gems) - end + def initialize(options) + @options = options + @glob = options[:glob] || "{,*/}*.gemspec" + @path = options[:path] + end - def ==(other) - other.is_a?(SystemGemSource) - end + def default_spec(*args) + return @default_spec if args.empty? + name, version = *args + @default_spec = Specification.new do |s| + s.name = name + s.source = self + s.version = Gem::Version.new(version) + s.relative_loaded_from = "#{name}.gemspec" + end + end - def to_s - "system" - end + def local_specs + @local_specs ||= begin + index = Index.new - def download(spec) - gemfile = Pathname.new(spec.loaded_from) - gemfile = gemfile.dirname.join('..', 'cache', "#{spec.full_name}.gem") - bundle.cache(gemfile) - end + if File.directory?(path) + Dir["#{path}/#{@glob}"].each do |file| + file = Pathname.new(file) + if spec = eval(File.read(file)) + spec = Specification.from_gemspec(spec) + spec.loaded_from = file + spec.source = self + index << spec + end + end - private + index << default_spec if default_spec && index.empty? + end - end + index.freeze + end + end - class GemDirectorySource < Source - attr_reader :location + alias specs local_specs - def initialize(bundle, options) - super - @location = options[:location] end - def local? - true - end + class Git < Path + attr_reader :uri, :ref - def gems - @specs ||= fetch_specs - end + def initialize(options) + @options = options + @glob = options[:glob] || "{,*/}*.gemspec" + @uri = options[:uri] + @ref = options[:ref] || options[:branch] || 'master' + end - def ==(other) - location == other.location - end - - def to_s - location.to_s - end - - def download(spec) - # raise NotImplementedError - end - - private - - def fetch_specs - specs = Hash.new { |h,k| h[k] = [] } - - Dir["#{@location}/*.gem"].each do |gemfile| - spec = Gem::Format.from_file_by_path(gemfile).spec - spec.source = self - specs[spec.name] << spec + def options + @options.merge(:ref => revision) end - specs - end - end - - class DirectorySource < Source - attr_reader :location, :specs, :required_specs - - def initialize(bundle, options) - super - if options[:location] - @location = Pathname.new(options[:location]).expand_path + def path + Bundler.install_path.join("#{base_name}-#{uri_hash}-#{ref}") end - @glob = options[:glob] || "**/*.gemspec" - @specs = {} - @required_specs = [] - end - def add_spec(path, name, version, require_paths = %w(lib)) - raise DirectorySourceError, "already have a gem defined for '#{path}'" if @specs[path.to_s] - @specs[path.to_s] = Gem::Specification.new do |s| - s.name = name - s.version = Gem::Version.new(version) + def to_s + @uri end - end - def local? - true - end + def specs + @specs ||= begin + index = Index.new + # Start by making sure the git cache is up to date + cache + # Find all gemspecs in the repo + in_cache do + out = %x(git ls-tree -r #{revision}).strip + lines = out.split("\n").select { |l| l =~ /\.gemspec$/ } + # Loop over the lines and extract the relative path and the + # git hash + lines.each do |line| + next unless line =~ %r{^(\d+) (blob|tree) ([a-zf0-9]+)\t(.*)$} + hash, file = $3, $4 + # Read the gemspec + if spec = eval(%x(git cat-file blob #{$3})) + spec = Specification.from_gemspec(spec) + spec.relative_loaded_from = file + spec.source = self + index << spec + end + end + end - def gems - @gems ||= begin - # Locate all gemspecs from the directory - specs = locate_gemspecs - specs = merge_defined_specs(specs) + index << default_spec if default_spec && index.empty? - required_specs.each do |required| - unless specs.any? {|k,v| v.name == required } - raise DirectorySourceError, "No gemspec for '#{required}' was found in" \ - " '#{location}'. Please explicitly specify a version." - end + index.freeze end - - process_source_gems(specs) end - end - def locate_gemspecs - Dir["#{location}/#{@glob}"].inject({}) do |specs, file| - file = Pathname.new(file) - if spec = eval(File.read(file)) # and validate_gemspec(file.dirname, spec) - spec.location = file.dirname.expand_path - specs[spec.full_name] = spec + def install(spec) + @installed ||= begin + FileUtils.mkdir_p(path) + Dir.chdir(path) do + unless File.exist?(".git") + %x(git clone --recursive --no-checkout #{cache_path} #{path}) + end + %x(git fetch --quiet) + %x(git reset --hard #{revision}) + %x(git submodule init) + %x(git submodule update) + end + true end - specs end - end - def merge_defined_specs(specs) - @specs.each do |path, spec| - # Set the spec location - spec.location = "#{location}/#{path}" + private - if existing = specs.values.find { |s| s.name == spec.name } - if existing.version != spec.version - raise DirectorySourceError, "The version you specified for #{spec.name}" \ - " is #{spec.version}. The gemspec is #{existing.version}." - # Not sure if this is needed - # ==== - # elsif File.expand_path(existing.location) != File.expand_path(spec.location) - # raise DirectorySourceError, "The location you specified for #{spec.name}" \ - # " is '#{spec.location}'. The gemspec was found at '#{existing.location}'." - end - # elsif !validate_gemspec(spec.location, spec) - # raise "Your gem definition is not valid: #{spec}" - else - specs[spec.full_name] = spec - end + def base_name + File.basename(uri, ".git") end - specs - end - def validate_gemspec(path, spec) - path = Pathname.new(path) - msg = "Gemspec for #{spec.name} (#{spec.version}) is invalid:" - # Check the require_paths - (spec.require_paths || []).each do |require_path| - unless path.join(require_path).directory? - Bundler.logger.warn "#{msg} Missing require path: '#{require_path}'" - return false - end + def uri_hash + Digest::SHA1.hexdigest(URI.parse(uri).normalize.to_s.sub(%r{/$}, '')) end - # Check the executables - (spec.executables || []).each do |exec| - unless path.join(spec.bindir, exec).file? - Bundler.logger.warn "#{msg} Missing executable: '#{File.join(spec.bindir, exec)}'" - return false - end + def cache_path + @cache_path ||= Bundler.cache.join("git", "#{base_name}-#{uri_hash}") end - true - end - - def ==(other) - # TMP HAX - other.is_a?(DirectorySource) - end - - def to_s - "directory: '#{location}'" - end - - def download(spec) - # Nothing needed here - end - end - - class GitSource < DirectorySource - attr_reader :ref, :uri, :branch - - def initialize(bundle, options) - super - @uri = options[:uri] - @branch = options[:branch] || 'master' - @ref = options[:ref] || "origin/#{@branch}" - end - - def local? - raise SourceNotCached, "Git repository '#{@uri}' has not been cloned yet" unless location.directory? - super - end - - def location - # TMP HAX to get the *.gemspec reading to work - bundle.gem_path.join('dirs', File.basename(@uri, '.git')) - end - - def gems - update if Bundler.remote? - checkout if Bundler.writable? - super - end - - def download(spec) - # Nothing needed here - end - - def to_s - "git: #{uri}" - end - - private - def update - if location.directory? - fetch + def cache + if cache_path.exist? + Bundler.ui.info "Source: Updating `#{uri}`... " + in_cache { `git fetch --quiet #{uri} master:master` } else - clone + Bundler.ui.info "Source: Cloning `#{uri}`... " + FileUtils.mkdir_p(cache_path.dirname) + `git clone #{uri} #{cache_path} --bare --no-hardlinks` end + Bundler.ui.info "Done." end - def fetch - Bundler.logger.info "Fetching git repository at: #{@uri}" - Dir.chdir(location) { `git fetch origin` } + def revision + @revision ||= in_cache { `git rev-parse #{ref}`.strip } end - def clone - Bundler.logger.info "Cloning git repository at: #{@uri}" - FileUtils.mkdir_p(location.dirname) - `git clone #{@uri} #{location} --no-hardlinks` + def in_cache(&blk) + Dir.chdir(cache_path, &blk) end - - def checkout - Dir.chdir(location) { `git checkout --quiet #{@ref}` } - end + end end -end +end \ No newline at end of file