diff --git a/lib/bundler.rb b/lib/bundler.rb index 78e2e875984..e68817a95aa 100644 --- a/lib/bundler.rb +++ b/lib/bundler.rb @@ -234,6 +234,10 @@ def app_cache(custom_path = nil) path.join(self.settings.app_cache_path) end + def global_cache + Pathname.new(File.expand_path(self.settings.global_cache_path)) + end + def tmp(name = Process.pid.to_s) Pathname.new(Dir.mktmpdir(["bundler", name])) end diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb index afa76fcb790..4b20f571ef6 100644 --- a/lib/bundler/settings.rb +++ b/lib/bundler/settings.rb @@ -222,6 +222,10 @@ def app_cache_path end end + def global_cache_path + self["path.global_cache"] || File.join(Bundler.rubygems.user_home, ".bundle/cache") + end + private def key_for(key) diff --git a/lib/bundler/source/rubygems.rb b/lib/bundler/source/rubygems.rb index 12d50e8017d..a9f8019e79e 100644 --- a/lib/bundler/source/rubygems.rb +++ b/lib/bundler/source/rubygems.rb @@ -79,7 +79,8 @@ def specs # sources, and large_idx.use small_idx is way faster than # small_idx.use large_idx. idx = @allow_remote ? remote_specs.dup : Index.new - idx.use(cached_specs, :override_dupes) if @allow_cached || @allow_remote + idx.use(cached_specs(:local), :override_dupes) if @allow_cached || @allow_remote + idx.use(cached_specs(:global), :override_dupes) idx.use(installed_specs, :override_dupes) idx end @@ -88,6 +89,7 @@ def specs def install(spec, opts = {}) force = opts[:force] ensure_builtin_gems_cached = opts[:ensure_builtin_gems_cached] + cache_globally(cached_path(spec), spec) if spec && cached_path(spec) if ensure_builtin_gems_cached && builtin_gem?(spec) if !cached_path(spec) @@ -306,12 +308,32 @@ def installed_specs end end - def cached_specs + def cache_globally(gemfile, spec = nil) + unless File.exist?("#{Bundler.global_cache}/#{File.basename(gemfile)}") + if spec + uri = spec.source.remotes.first + source_dir = [uri.hostname, uri.port, Digest::MD5.hexdigest(uri.path)].compact.join(".") + cache_dir = Bundler.global_cache.join("gems", source_dir) + else + cache_dir = Bundler.global_cache.join("gems") + end + FileUtils.mkdir_p(cache_dir) + FileUtils.cp(gemfile, cache_dir) + end + end + + def cached_specs(scope) @cached_specs ||= begin idx = installed_specs.dup - - path = Bundler.app_cache - Dir["#{path}/*.gem"].each do |gemfile| + path = + case scope + when :local then Bundler.app_cache + when :global then Bundler.global_cache + else + raise "scope must be :local or :global" + end + + Dir["#{path}/**/*.gem"].each do |gemfile| next if gemfile =~ /^bundler\-[\d\.]+?\.gem/ s ||= Bundler.rubygems.spec_from_gem(gemfile) s.source = self @@ -397,13 +419,16 @@ def fetch_gem(spec) spec.fetch_platform download_path = Bundler.requires_sudo? ? Bundler.tmp(spec.full_name) : Bundler.rubygems.gem_dir - gem_path = "#{Bundler.rubygems.gem_dir}/cache/#{spec.full_name}.gem" + local_gem_path = "#{Bundler.rubygems.gem_dir}/cache" + gem_path = "#{local_gem_path}/#{spec.full_name}.gem" FileUtils.mkdir_p("#{download_path}/cache") Bundler.rubygems.download_gem(spec, uri, download_path) + cache_globally(gem_path, spec) + # TODO: Maybe do something in this method: Check when download_gem is called? if Bundler.requires_sudo? - Bundler.mkdir_p "#{Bundler.rubygems.gem_dir}/cache" + Bundler.mkdir_p local_gem_path Bundler.sudo "mv #{download_path}/cache/#{spec.full_name}.gem #{gem_path}" end diff --git a/spec/commands/install_spec.rb b/spec/commands/install_spec.rb index 7e36eeeed02..7c6e1dc2328 100644 --- a/spec/commands/install_spec.rb +++ b/spec/commands/install_spec.rb @@ -404,4 +404,162 @@ expect(err).to include("Please use `bundle config path") end end + + describe "using the global cache" do + let(:source_hostname) { "localgemserver.test" } + let(:source_uri) { "http://#{source_hostname}" } + + it "creates the global cache directory" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0" + G + + bundle :install + expect(bundle_cache).to exist + end + + it "copies .gem files to the global cache" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0" + G + + bundle :install + expect(bundle_cached_gem("rack-1.0.0", gem_repo1)).to exist + end + + it "does not remove .gem files from the global cache" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0" + G + + bundle :install + expect(bundle_cached_gem("rack-1.0.0", gem_repo1)).to exist + + gemfile <<-G + source "file://#{gem_repo1}" + G + + bundle :install + expect(bundle_cached_gem("rack-1.0.0", gem_repo1)).to exist + end + + # FIXME: check what behavior is being tested + it "does not download gems to the global cache when caching globally" do + gemfile <<-G + source "#{source_uri}" + gem "rack", "1.0" + G + + bundle :install, :artifice => "endpoint" + expect(out).to include("Fetching gem metadata from #{source_uri}") + expect(bundle_cached_gem("rack-1.0.0", source_uri)).to exist + FileUtils.rm_r(bundle_cache) + expect(bundle_cache).not_to exist + + bundle :install, :artifice => "endpoint" + expect(out).not_to include("Fetching gem metadata from #{source_uri}") + expect(bundle_cached_gem("rack-1.0.0", source_uri)).to exist + end + + it "uses the global cache as a source when installing gems" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0" + G + + FileUtils.rm_r(default_bundle_path) + # build_gem "rack", :path => bundle_cache_source_dir("https://rubygems.org") + + install_gemfile <<-G, :artifice => "endpoint_no_gem" + source "https://rubygems.org" + gem "rack" + G + $stderr.puts out + + should_be_installed "rack 1.0.0" + end + + it "uses the global cache as a source when installing local gems from a different directory" do + build_gem "omg", :path => bundle_cache_source_dir(gem_repo1) + build_gem "foo", :path => bundle_cache_source_dir(gem_repo1) + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "omg" + G + + should_be_installed "omg 1.0.0" + should_not_be_installed "foo 1.0.0" + + Dir.chdir bundled_app2 do + create_file "gems.rb", Pathname.new(bundled_app2("gems.rb")), <<-G + source "file://#{gem_repo1}" + gem "foo" + G + + should_not_be_installed "omg 1.0.0" + should_not_be_installed "foo 1.0.0" + + bundle :install + + should_be_installed "foo 1.0.0" + should_not_be_installed "omg 1.0.0" + end + end + + it "uses the global cache as a source when installing remote gems from a different directory" do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + should_be_installed "rack 1.0.0" + + Dir.chdir bundled_app2 do + create_file "gems.rb", Pathname.new(bundled_app2("gems.rb")), <<-G + source "#{source_uri}" + gem "rack" + G + + should_not_be_installed "rack 1.0.0" + + bundle :install, :artifice => "endpoint_no_gem" + expect(out).not_to include("Fetching gem metadata from #{source_uri}") + should_be_installed "rack 1.0.0" + end + end + + it "allows the global cache path to be configured" do + bundle "config path.global_cache #{home}/machine_cache" + build_gem "omg", :path => "#{home}/machine_cache/gems/#{source_dir(gem_repo1)}" + + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "omg" + G + + should_be_installed "omg 1.0.0" + end + + it "copies gems from the local cache to the global cache" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack", "1.0" + G + + bundle :install + bundle :cache + FileUtils.rm_r(default_bundle_path) + FileUtils.rm_r(bundle_cache) + expect(default_bundle_path).not_to exist + expect(bundle_cache).not_to exist + expect(cached_gem("rack-1.0.0")).to exist + + bundle :install + expect(bundle_cached_gem("rack-1.0.0", gem_repo1)).to exist + end + end end diff --git a/spec/support/artifice/endpoint_no_gem.rb b/spec/support/artifice/endpoint_no_gem.rb new file mode 100644 index 00000000000..54efbb2193d --- /dev/null +++ b/spec/support/artifice/endpoint_no_gem.rb @@ -0,0 +1,11 @@ +require File.expand_path("../endpoint", __FILE__) + +Artifice.deactivate + +class EndpointNoGem < Endpoint + get "/gems/:id" do + halt 500 + end +end + +Artifice.activate_with(EndpointNoGem) diff --git a/spec/support/path.rb b/spec/support/path.rb index bebc9c4146f..0703909c1f7 100644 --- a/spec/support/path.rb +++ b/spec/support/path.rb @@ -40,6 +40,28 @@ def cached_gem(path) bundled_app("vendor/cache/#{path}.gem") end + def bundle_cache(*path) + home(".bundle/cache", *path) + end + + def source_dir(source) + prefix = %r(https?:\/\/) =~ source.to_s ? "" : "file:" + uri = Bundler::Source::Rubygems::Remote.new(URI("#{prefix}#{source}/")).uri + [uri.hostname, uri.port, Digest::MD5.hexdigest(uri.path)].compact.join(".") + end + + def bundle_cache_source_dir(source) + bundle_cache("gems", source_dir(source)) + end + + def bundle_cached_gem(gem, source = nil) + if source + bundle_cache_source_dir(source).join("#{gem}.gem") + else + bundle_cache("gems", "#{gem}.gem") + end + end + def base_system_gems tmp.join("gems/base") end