lib/bundler/source.rb



require "uri"
require 'rubygems/user_interaction'
require "rubygems/installer"
require "rubygems/spec_fetcher"
require "rubygems/format"
require "digest/sha1"
require "fileutils"

module Bundler
  module Source
    # TODO: Refactor this class
    class Rubygems
      FORCE_MODERN_INDEX_LIMIT = 100 # threshold for switching back to the modern index instead of fetching every spec

      attr_reader :remotes, :caches
      attr_accessor :dependency_names

      def initialize(options = {})
        @options = options
        @remotes = (options["remotes"] || []).map { |r| normalize_uri(r) }
        @fetchers = {}
        @allow_remote = false
        @allow_cached = false

        @caches = [ Bundler.app_cache ] +
          Bundler.rubygems.gem_path.map{|p| File.expand_path("#{p}/cache") }
      end

      def remote!
        @allow_remote = true
      end

      def cached!
        @allow_cached = true
      end

      def hash
        Rubygems.hash
      end

      def eql?(o)
        Rubygems === o
      end

      alias == eql?

      def options
        { "remotes" => @remotes.map { |r| r.to_s } }
      end

      def self.from_lock(options)
        s = new(options)
        Array(options["remote"]).each { |r| s.add_remote(r) }
        s
      end

      def to_lock
        out = "GEM\n"
        out << remotes.map {|r| "  remote: #{r}\n" }.join
        out << "  specs:\n"
      end

      def to_s
        remote_names = self.remotes.map { |r| r.to_s }.join(', ')
        "rubygems repository #{remote_names}"
      end
      alias_method :name, :to_s

      def specs
        @specs ||= fetch_specs
      end

      def install(spec)
        if installed_specs[spec].any?
          Bundler.ui.info "Using #{spec.name} (#{spec.version}) "
          return
        end

        Bundler.ui.info "Installing #{spec.name} (#{spec.version}) "
        path = cached_gem(spec)
        if Bundler.requires_sudo?
          install_path = Bundler.tmp
          bin_path     = install_path.join("bin")
        else
          install_path = Bundler.rubygems.gem_dir
          bin_path     = Bundler.system_bindir
        end

        Bundler.rubygems.preserve_paths do
          Bundler::GemInstaller.new(path,
            :install_dir         => install_path.to_s,
            :bin_dir             => bin_path.to_s,
            :ignore_dependencies => true,
            :wrappers            => true,
            :env_shebang         => true
          ).install
        end

        if spec.post_install_message
          Installer.post_install_messages[spec.name] = spec.post_install_message
        end

        # SUDO HAX
        if Bundler.requires_sudo?
          Bundler.mkdir_p "#{Bundler.rubygems.gem_dir}/gems"
          Bundler.mkdir_p "#{Bundler.rubygems.gem_dir}/specifications"
          Bundler.sudo "cp -R #{Bundler.tmp}/gems/#{spec.full_name} #{Bundler.rubygems.gem_dir}/gems/"
          Bundler.sudo "cp -R #{Bundler.tmp}/specifications/#{spec.full_name}.gemspec #{Bundler.rubygems.gem_dir}/specifications/"
          spec.executables.each do |exe|
            Bundler.mkdir_p Bundler.system_bindir
            Bundler.sudo "cp -R #{Bundler.tmp}/bin/#{exe} #{Bundler.system_bindir}"
          end
        end

        spec.loaded_from = "#{Bundler.rubygems.gem_dir}/specifications/#{spec.full_name}.gemspec"
      end

      def cache(spec)
        cached_path = cached_gem(spec)
        raise GemNotFound, "Missing gem file '#{spec.full_name}.gem'." unless cached_path
        return if File.dirname(cached_path) == Bundler.app_cache.to_s
        Bundler.ui.info "  * #{File.basename(cached_path)}"
        FileUtils.cp(cached_path, Bundler.app_cache)
      end

      def add_remote(source)
        @remotes << normalize_uri(source)
      end

      def replace_remotes(source)
        return false if source.remotes == @remotes

        @remotes = []
        source.remotes.each do |r|
          add_remote r.to_s
        end

        true
      end

    private

      def cached_gem(spec)
        possibilities = @caches.map { |p| "#{p}/#{spec.file_name}" }
        cached_gem = possibilities.find { |p| File.exist?(p) }
        unless cached_gem
          raise Bundler::GemNotFound, "Could not find #{spec.file_name} for installation"
        end
        cached_gem
      end

      def normalize_uri(uri)
        uri = uri.to_s
        uri = "#{uri}/" unless uri =~ %r'/$'
        uri = URI(uri)
        raise ArgumentError, "The source must be an absolute URI" unless uri.absolute?
        uri
      end

      def fetch_specs
        # remote_specs usually generates a way larger Index than the other
        # sources, and large_idx.use small_idx is way faster than
        # small_idx.use large_idx.
        if @allow_remote
          idx = remote_specs.dup
        else
          idx = Index.new
        end
        idx.use(cached_specs, :override_dupes) if @allow_cached || @allow_remote
        idx.use(installed_specs, :override_dupes)
        idx
      end

      def installed_specs
        @installed_specs ||= begin
          idx = Index.new
          have_bundler = false
          Bundler.rubygems.all_specs.reverse.each do |spec|
            next if spec.name == 'bundler' && spec.version.to_s != VERSION
            have_bundler = true if spec.name == 'bundler'
            spec.source = self
            idx << spec
          end

          # Always have bundler locally
          unless have_bundler
           # We're running bundler directly from the source
           # so, let's create a fake gemspec for it (it's a path)
           # gemspec
           bundler = Gem::Specification.new do |s|
             s.name     = 'bundler'
             s.version  = VERSION
             s.platform = Gem::Platform::RUBY
             s.source   = self
             s.authors  = ["bundler team"]
             s.loaded_from = File.expand_path("..", __FILE__)
           end
           idx << bundler
          end
          idx
        end
      end

      def cached_specs
        @cached_specs ||= begin
          idx = installed_specs.dup

          path = Bundler.app_cache
          Dir["#{path}/*.gem"].each do |gemfile|
            next if gemfile =~ /^bundler\-[\d\.]+?\.gem/

            begin
              s ||= Bundler.rubygems.spec_from_gem(gemfile)
            rescue Gem::Package::FormatError
              raise GemspecError, "Could not read gem at #{gemfile}. It may be corrupted."
            end

            s.source = self
            idx << s
          end
        end

        idx
      end

      def remote_specs
        @remote_specs ||= begin
          idx     = Index.new
          old     = Bundler.rubygems.sources

          sources = {}
          remotes.each do |uri|
            fetcher          = Bundler::Fetcher.new(uri)
            specs            = fetcher.specs(dependency_names, self)
            sources[fetcher] = specs.size

            idx.use specs
          end

          # don't need to fetch all specifications for every gem/version on
          # the rubygems repo if there's no api endpoints to search over
          # or it has too many specs to fetch
          fetchers              = sources.keys
          api_fetchers          = fetchers.select {|fetcher| fetcher.has_api }
          modern_index_fetchers = fetchers - api_fetchers
          if api_fetchers.any? && modern_index_fetchers.all? {|fetcher| sources[fetcher] < FORCE_MODERN_INDEX_LIMIT }
            # this will fetch all the specifications on the rubygems repo
            unmet_dependency_names = idx.unmet_dependency_names
            unmet_dependency_names -= ['bundler'] # bundler will always be unmet

            Bundler.ui.debug "Unmet Dependencies: #{unmet_dependency_names}"
            if unmet_dependency_names.any?
              api_fetchers.each do |fetcher|
                idx.use fetcher.specs(unmet_dependency_names, self)
              end
            end
          else
            Bundler::Fetcher.disable_endpoint = true
            api_fetchers.each {|fetcher| idx.use fetcher.specs([], self) }
          end

          idx
        ensure
          Bundler.rubygems.sources = old
        end
      end
    end


    class Path
      class Installer < Bundler::GemInstaller
        def initialize(spec, options = {})
          @spec              = spec
          @bin_dir           = Bundler.requires_sudo? ? "#{Bundler.tmp}/bin" : "#{Bundler.rubygems.gem_dir}/bin"
          @gem_dir           = Bundler.rubygems.path(spec.full_gem_path)
          @wrappers          = options[:wrappers] || true
          @env_shebang       = options[:env_shebang] || true
          @format_executable = options[:format_executable] || false
        end

        def generate_bin
          return if spec.executables.nil? || spec.executables.empty?

          if Bundler.requires_sudo?
            FileUtils.mkdir_p("#{Bundler.tmp}/bin") unless File.exist?("#{Bundler.tmp}/bin")
          end
          super
          if Bundler.requires_sudo?
            Bundler.mkdir_p "#{Bundler.rubygems.gem_dir}/bin"
            spec.executables.each do |exe|
              Bundler.sudo "cp -R #{Bundler.tmp}/bin/#{exe} #{Bundler.rubygems.gem_dir}/bin/"
            end
          end
        end
      end

      attr_reader   :path, :options
      attr_writer   :name
      attr_accessor :version

      DEFAULT_GLOB = "{,*,*/*}.gemspec"

      def initialize(options)
        @options = options
        @glob = options["glob"] || DEFAULT_GLOB

        @allow_cached = false
        @allow_remote = false

        if options["path"]
          @path = Pathname.new(options["path"])
          @path = @path.expand_path(Bundler.root) unless @path.relative?
        end

        @name    = options["name"]
        @version = options["version"]

        # Stores the original path. If at any point we move to the
        # cached directory, we still have the original path to copy from.
        @original_path = @path
      end

      def remote!
        @allow_remote = true
      end

      def cached!
        @allow_cached = true
      end

      def self.from_lock(options)
        new(options.merge("path" => options.delete("remote")))
      end

      def to_lock
        out = "PATH\n"
        out << "  remote: #{relative_path}\n"
        out << "  glob: #{@glob}\n" unless @glob == DEFAULT_GLOB
        out << "  specs:\n"
      end

      def to_s
        "source at #{@path}"
      end

      def hash
        self.class.hash
      end

      def eql?(o)
        o.instance_of?(Path) &&
        path.expand_path(Bundler.root) == o.path.expand_path(Bundler.root) &&
        version == o.version
      end

      alias == eql?

      def name
        File.basename(path.expand_path(Bundler.root).to_s)
      end

      def install(spec)
        Bundler.ui.info "Using #{spec.name} (#{spec.version}) from #{to_s} "
        # Let's be honest, when we're working from a path, we can't
        # really expect native extensions to work because the whole point
        # is to just be able to modify what's in that path and go. So, let's
        # not put ourselves through the pain of actually trying to generate
        # the full gem.
        Installer.new(spec).generate_bin
      end

      def cache(spec)
        return unless Bundler.settings[:cache_all]
        return if @original_path.expand_path(Bundler.root).to_s.index(Bundler.root.to_s) == 0
        FileUtils.rm_rf(app_cache_path)
        FileUtils.cp_r("#{@original_path}/.", app_cache_path)
        FileUtils.touch(app_cache_path.join(".bundlecache"))
      end

      def local_specs(*)
        @local_specs ||= load_spec_files
      end

      def specs
        if has_app_cache?
          @path = app_cache_path
        end
        local_specs
      end

      def app_cache_dirname
        name
      end

    private

      def app_cache_path
        @app_cache_path ||= Bundler.app_cache.join(app_cache_dirname)
      end

      def has_app_cache?
        SharedHelpers.in_bundle? && app_cache_path.exist?
      end

      def load_spec_files
        index = Index.new
        expanded_path = path.expand_path(Bundler.root)

        if File.directory?(expanded_path)
          Dir["#{expanded_path}/#{@glob}"].each do |file|
            spec = Bundler.load_gemspec(file)
            if spec
              spec.loaded_from = file.to_s
              spec.source = self
              index << spec
            end
          end

          if index.empty? && @name && @version
            index << Gem::Specification.new do |s|
              s.name     = @name
              s.source   = self
              s.version  = Gem::Version.new(@version)
              s.platform = Gem::Platform::RUBY
              s.summary  = "Fake gemspec for #{@name}"
              s.relative_loaded_from = "#{@name}.gemspec"
              s.authors  = ["no one"]
              if expanded_path.join("bin").exist?
                executables = expanded_path.join("bin").children
                executables.reject!{|p| File.directory?(p) }
                s.executables = executables.map{|c| c.basename.to_s }
              end
            end
          end
        else
          raise PathError, "The path `#{expanded_path}` does not exist."
        end

        index
      end

      def relative_path
        if path.to_s.match(%r{^#{Regexp.escape Bundler.root.to_s}})
          return path.relative_path_from(Bundler.root)
        end
        path
      end

      def generate_bin(spec)
        gem_dir  = Pathname.new(spec.full_gem_path)

        # Some gem authors put absolute paths in their gemspec
        # and we have to save them from themselves
        spec.files = spec.files.map do |p|
          next if File.directory?(p)
          begin
            Pathname.new(p).relative_path_from(gem_dir).to_s
          rescue ArgumentError
            p
          end
        end.compact

        gem_file = Dir.chdir(gem_dir){ Gem::Builder.new(spec).build }

        installer = Path::Installer.new(spec, :env_shebang => false)
        run_hooks(:pre_install, installer)
        installer.build_extensions
        run_hooks(:post_build, installer)
        installer.generate_bin
        run_hooks(:post_install, installer)
      rescue Gem::InvalidSpecificationException => e
        Bundler.ui.warn "\n#{spec.name} at #{spec.full_gem_path} did not have a valid gemspec.\n" \
                        "This prevents bundler from installing bins or native extensions, but " \
                        "that may not affect its functionality."

        if !spec.extensions.empty? && !spec.email.empty?
          Bundler.ui.warn "If you need to use this package without installing it from a gem " \
                          "repository, please contact #{spec.email} and ask them " \
                          "to modify their .gemspec so it can work with `gem build`."
        end

        Bundler.ui.warn "The validation message from Rubygems was:\n  #{e.message}"
      ensure
        Dir.chdir(gem_dir){ FileUtils.rm_rf(gem_file) if gem_file && File.exist?(gem_file) }
      end

      def run_hooks(type, installer)
        hooks_meth = "#{type}_hooks"
        return unless Gem.respond_to?(hooks_meth)
        Gem.send(hooks_meth).each do |hook|
          result = hook.call(installer)
          if result == false
            location = " at #{$1}" if hook.inspect =~ /@(.*:\d+)/
            message = "#{type} hook#{location} failed for #{installer.spec.full_name}"
            raise InstallHookError, message
          end
        end
      end
    end

    class Git < Path
      # The GitProxy is responsible to iteract with git repositories.
      # All actions required by the Git source is encapsualted in this
      # object.
      class GitProxy
        attr_accessor :path, :uri, :ref, :revision

        def initialize(path, uri, ref, revision=nil, &allow)
          @path     = path
          @uri      = uri
          @ref      = ref
          @revision = revision
          @allow    = allow || Proc.new { true }
        end

        def revision
          @revision ||= allowed_in_path { git("rev-parse #{ref}").strip }
        end

        def branch
          @branch ||= allowed_in_path do
            git("branch") =~ /^\* (.*)$/ && $1.strip
          end
        end

        def contains?(commit)
          allowed_in_path do
            result = git_null("branch --contains #{commit}")
            $? == 0 && result =~ /^\* (.*)$/
          end
        end

        def checkout
          if path.exist?
            return if has_revision_cached?
            Bundler.ui.info "Updating #{uri}"
            in_path do
              git %|fetch --force --quiet --tags #{uri_escaped} "refs/heads/*:refs/heads/*"|
            end
          else
            Bundler.ui.info "Fetching #{uri}"
            FileUtils.mkdir_p(path.dirname)
            git %|clone #{uri_escaped} "#{path}" --bare --no-hardlinks|
          end
        end

        def copy_to(destination, submodules=false)
          unless File.exist?(destination.join(".git"))
            FileUtils.mkdir_p(destination.dirname)
            FileUtils.rm_rf(destination)
            git %|clone --no-checkout "#{path}" "#{destination}"|
            File.chmod((0777 & ~File.umask), destination)
          end

          Dir.chdir(destination) do
            git %|fetch --force --quiet --tags "#{path}"|
            git "reset --hard #{@revision}"

            if submodules
              git "submodule update --init --recursive"
            end
          end
        end

      private

        # TODO: Do not rely on /dev/null.
        # Given that open3 is not cross platform until Ruby 1.9.3,
        # the best solution is to pipe to /dev/null if it exists.
        # If it doesn't, everything will work fine, but the user
        # will get the $stderr messages as well.
        def git_null(command)
          if !Bundler::WINDOWS && File.exist?("/dev/null")
            git("#{command} 2>/dev/null", false)
          else
            git(command, false)
          end
        end

        def git(command, check_errors=true)
          if allow?
            out = %x{git #{command}}

            if check_errors && $?.exitstatus != 0
              msg = "Git error: command `git #{command}` in directory #{Dir.pwd} has failed."
              msg << "\nIf this error persists you could try removing the cache directory '#{path}'" if path.exist?
              raise GitError, msg
            end
            out
          else
            raise GitError, "Bundler is trying to run a `git #{command}` at runtime. You probably need to run `bundle install`. However, " \
                            "this error message could probably be more useful. Please submit a ticket at http://github.com/carlhuda/bundler/issues " \
                            "with steps to reproduce as well as the following\n\nCALLER: #{caller.join("\n")}"
          end
        end

        def has_revision_cached?
          return unless @revision
          in_path { git("cat-file -e #{@revision}") }
          true
        rescue GitError
          false
        end

        # Escape the URI for git commands
        def uri_escaped
          if Bundler::WINDOWS
            # Windows quoting requires double quotes only, with double quotes
            # inside the string escaped by being doubled.
            '"' + uri.gsub('"') {|s| '""'} + '"'
          else
            # Bash requires single quoted strings, with the single quotes escaped
            # by ending the string, escaping the quote, and restarting the string.
            "'" + uri.gsub("'") {|s| "'\\''"} + "'"
          end
        end

        def allow?
          @allow.call
        end

        def in_path(&blk)
          checkout unless path.exist?
          Dir.chdir(path, &blk)
        end

        def allowed_in_path
          if allow?
            in_path { yield }
          else
            raise GitError, "The git source #{uri} is not yet checked out. Please run `bundle install` before trying to start your application"
          end
        end
      end

      attr_reader :uri, :ref, :branch, :options, :submodules

      def initialize(options)
        @options = options
        @glob = options["glob"] || DEFAULT_GLOB

        @allow_cached = false
        @allow_remote = false

        # Stringify options that could be set as symbols
        %w(ref branch tag revision).each{|k| options[k] = options[k].to_s if options[k] }

        @uri        = options["uri"]
        @branch     = options["branch"]
        @ref        = options["ref"] || options["branch"] || options["tag"] || 'master'
        @submodules = options["submodules"]
        @name       = options["name"]
        @version    = options["version"]

        @update     = false
        @installed  = nil
        @local      = false
      end

      def self.from_lock(options)
        new(options.merge("uri" => options.delete("remote")))
      end

      def to_lock
        out = "GIT\n"
        out << "  remote: #{@uri}\n"
        out << "  revision: #{revision}\n"
        %w(ref branch tag submodules).each do |opt|
          out << "  #{opt}: #{options[opt]}\n" if options[opt]
        end
        out << "  glob: #{@glob}\n" unless @glob == DEFAULT_GLOB
        out << "  specs:\n"
      end

      def eql?(o)
        Git === o            &&
        uri == o.uri         &&
        ref == o.ref         &&
        branch == o.branch   &&
        name == o.name       &&
        version == o.version &&
        submodules == o.submodules
      end

      alias == eql?

      def to_s
        at = if local?
          path
        elsif options["ref"]
          shortref_for_display(options["ref"])
        else
          ref
        end
        "#{uri} (at #{at})"
      end

      def name
        File.basename(@uri, '.git')
      end

      # This is the path which is going to contain a specific
      # checkout of the git repository. When using local git
      # repos, this is set to the local repo.
      def install_path
        @install_path ||= begin
          git_scope = "#{base_name}-#{shortref_for_path(revision)}"

          if Bundler.requires_sudo?
            Bundler.user_bundle_path.join(Bundler.ruby_scope).join(git_scope)
          else
            Bundler.install_path.join(git_scope)
          end
        end
      end

      alias :path :install_path

      def unlock!
        git_proxy.revision = nil
      end

      def local_override!(path)
        return false if local?

        path = Pathname.new(path)
        path = path.expand_path(Bundler.root) unless path.relative?

        unless options["branch"] || Bundler.settings[:disable_local_branch_check]
          raise GitError, "Cannot use local override for #{name} at #{path} because " \
            ":branch is not specified in Gemfile. Specify a branch or use " \
            "`bundle config --delete` to remove the local override"
        end

        unless path.exist?
          raise GitError, "Cannot use local override for #{name} because #{path} " \
            "does not exist. Check `bundle config --delete` to remove the local override"
        end

        set_local!(path)

        # Create a new git proxy without the cached revision
        # so the Gemfile.lock always picks up the new revision.
        @git_proxy = GitProxy.new(path, uri, ref)

        if git_proxy.branch != options["branch"] && !Bundler.settings[:disable_local_branch_check]
          raise GitError, "Local override for #{name} at #{path} is using branch " \
            "#{git_proxy.branch} but Gemfile specifies #{options["branch"]}"
        end

        changed = cached_revision && cached_revision != git_proxy.revision

        if changed && !git_proxy.contains?(cached_revision)
          raise GitError, "The Gemfile lock is pointing to revision #{shortref_for_display(cached_revision)} " \
            "but the current branch in your local override for #{name} does not contain such commit. " \
            "Please make sure your branch is up to date."
        end

        changed
      end

      # TODO: actually cache git specs
      def specs(*)
        if has_app_cache? && !local?
          set_local!(app_cache_path)
        end

        if requires_checkout? && !@update
          git_proxy.checkout
          git_proxy.copy_to(install_path, submodules)
          @update = true
        end

        local_specs
      end

      def install(spec)
        Bundler.ui.info "Using #{spec.name} (#{spec.version}) from #{to_s} "
        if requires_checkout? && !@installed
          Bundler.ui.debug "  * Checking out revision: #{ref}"
          git_proxy.copy_to(install_path, submodules)
          @installed = true
        end
        generate_bin(spec)
      end

      def cache(spec)
        return unless Bundler.settings[:cache_all]
        return if path == app_cache_path
        cached!
        FileUtils.rm_rf(app_cache_path)
        git_proxy.checkout if requires_checkout?
        git_proxy.copy_to(app_cache_path, @submodules)
        FileUtils.rm_rf(app_cache_path.join(".git"))
        FileUtils.touch(app_cache_path.join(".bundlecache"))
      end

      def load_spec_files
        super
      rescue PathError, GitError
        raise GitError, "#{to_s} is not checked out. Please run `bundle install`"
      end

      # This is the path which is going to contain a cache
      # of the git repository. When using the same git repository
      # across different projects, this cache will be shared.
      # When using local git repos, this is set to the local repo.
      def cache_path
        @cache_path ||= begin
          git_scope = "#{base_name}-#{uri_hash}"

          if Bundler.requires_sudo?
            Bundler.user_bundle_path.join("cache/git", git_scope)
          else
            Bundler.cache.join("git", git_scope)
          end
        end
      end

      def app_cache_dirname
        "#{base_name}-#{shortref_for_path(cached_revision || revision)}"
      end

    private

      def set_local!(path)
        @local       = true
        @local_specs = @git_proxy = nil
        @cache_path  = @install_path = path
      end

      def has_app_cache?
        cached_revision && super
      end

      def local?
        @local
      end

      def requires_checkout?
        allow_git_ops? && !local?
      end

      def base_name
        File.basename(uri.sub(%r{^(\w+://)?([^/:]+:)?(//\w*/)?(\w*/)*},''),".git")
      end

      def shortref_for_display(ref)
        ref[0..6]
      end

      def shortref_for_path(ref)
        ref[0..11]
      end

      def uri_hash
        if uri =~ %r{^\w+://(\w+@)?}
          # Downcase the domain component of the URI
          # and strip off a trailing slash, if one is present
          input = URI.parse(uri).normalize.to_s.sub(%r{/$},'')
        else
          # If there is no URI scheme, assume it is an ssh/git URI
          input = uri
        end
        Digest::SHA1.hexdigest(input)
      end

      def allow_git_ops?
        @allow_remote || @allow_cached
      end

      def cached_revision
        options["revision"]
      end

      def revision
        git_proxy.revision
      end

      def cached?
        cache_path.exist?
      end

      def git_proxy
        @git_proxy ||= GitProxy.new(cache_path, uri, ref, cached_revision){ allow_git_ops? }
      end
    end
  end
end