diff options
author | Andre Arko <andre@arko.net> | 2012-11-13 10:38:48 -0800 |
---|---|---|
committer | Andre Arko <andre@arko.net> | 2012-11-13 10:40:34 -0800 |
commit | 4b4916c03b75674f06a15e1180334756b96dfca8 (patch) | |
tree | 3befedf739c742be25e8ee02d8c9882e477d4cdf /lib | |
parent | 405edfe8995955a8161dc2cc815337c512901c78 (diff) | |
download | bundler-4b4916c03b75674f06a15e1180334756b96dfca8.tar.gz |
extract classes in bundler/source for autoloading
Diffstat (limited to 'lib')
-rw-r--r-- | lib/bundler/source.rb | 889 | ||||
-rw-r--r-- | lib/bundler/source/git.rb | 267 | ||||
-rw-r--r-- | lib/bundler/source/git/git_proxy.rb | 142 | ||||
-rw-r--r-- | lib/bundler/source/path.rb | 212 | ||||
-rw-r--r-- | lib/bundler/source/path/installer.rb | 33 | ||||
-rw-r--r-- | lib/bundler/source/rubygems.rb | 266 |
6 files changed, 923 insertions, 886 deletions
diff --git a/lib/bundler/source.rb b/lib/bundler/source.rb index 004a3342..c75599f4 100644 --- a/lib/bundler/source.rb +++ b/lib/bundler/source.rb @@ -1,890 +1,7 @@ -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 - attr_writer :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 + autoload :Rubygems, 'bundler/source/rubygems' + autoload :Path, 'bundler/source/path' + autoload :Git, 'bundler/source/git' end end diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb new file mode 100644 index 00000000..0a53a34b --- /dev/null +++ b/lib/bundler/source/git.rb @@ -0,0 +1,267 @@ +require 'fileutils' +require 'uri' +require 'digest/sha1' + +module Bundler + module Source + + class Git < Path + autoload :GitProxy, 'bundler/source/git/git_proxy' + + 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
\ No newline at end of file diff --git a/lib/bundler/source/git/git_proxy.rb b/lib/bundler/source/git/git_proxy.rb new file mode 100644 index 00000000..385e4ed1 --- /dev/null +++ b/lib/bundler/source/git/git_proxy.rb @@ -0,0 +1,142 @@ +module Bundler + module Source + + 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 + attr_writer :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 + + end + end +end
\ No newline at end of file diff --git a/lib/bundler/source/path.rb b/lib/bundler/source/path.rb new file mode 100644 index 00000000..084419f9 --- /dev/null +++ b/lib/bundler/source/path.rb @@ -0,0 +1,212 @@ +module Bundler + module Source + + class Path + autoload :Installer, 'bundler/source/path/installer' + + 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 + + end +end
\ No newline at end of file diff --git a/lib/bundler/source/path/installer.rb b/lib/bundler/source/path/installer.rb new file mode 100644 index 00000000..962b24dc --- /dev/null +++ b/lib/bundler/source/path/installer.rb @@ -0,0 +1,33 @@ +module Bundler + module Source + 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 + + end + end +end
\ No newline at end of file diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb new file mode 100644 index 00000000..3455f4d2 --- /dev/null +++ b/lib/bundler/source/rubygems.rb @@ -0,0 +1,266 @@ +require 'uri' +require 'rubygems/user_interaction' +require 'rubygems/spec_fetcher' + +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 + + end +end
\ No newline at end of file |