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/bundler/source.rb | |
parent | 405edfe8995955a8161dc2cc815337c512901c78 (diff) | |
download | bundler-4b4916c03b75674f06a15e1180334756b96dfca8.tar.gz |
extract classes in bundler/source for autoloading
Diffstat (limited to 'lib/bundler/source.rb')
-rw-r--r-- | lib/bundler/source.rb | 889 |
1 files changed, 3 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 |