Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion bundler/lib/bundler/source/rubygems.rb
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,12 @@ def download_cache_path(spec)
return unless remote = spec.remote
return unless cache_slug = remote.cache_slug

Bundler.user_cache.join("gems", cache_slug)
if Gem.respond_to?(:global_gem_cache_path)
Pathname.new(Gem.global_gem_cache_path).join(cache_slug)
else
# Fall back to old location for older RubyGems versions
Bundler.user_cache.join("gems", cache_slug)
end
end

def extension_cache_slug(spec)
Expand Down
2 changes: 1 addition & 1 deletion bundler/spec/install/global_cache_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
let(:source2) { "http://gemserver.example.org" }

def cache_base
home(".bundle", "cache", "gems")
home(".cache", "gem", "gems")
end

def source_global_cache(*segments)
Expand Down
13 changes: 11 additions & 2 deletions lib/rubygems/config_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class Gem::ConfigFile
DEFAULT_IPV4_FALLBACK_ENABLED = false
# TODO: Use false as default value for this option in RubyGems 4.0
DEFAULT_INSTALL_EXTENSION_IN_LIB = true
DEFAULT_GLOBAL_GEM_CACHE = false

##
# For Ruby packagers to set configuration defaults. Set in
Expand Down Expand Up @@ -155,6 +156,12 @@ class Gem::ConfigFile

attr_accessor :ipv4_fallback_enabled

##
# Use a global cache for .gem files shared across all Ruby installations.
# When enabled, gems are cached to ~/.cache/gem/gems (or XDG_CACHE_HOME/gem/gems).

attr_accessor :global_gem_cache

##
# Path name of directory or file of openssl client certificate, used for remote https connection with client authentication

Expand Down Expand Up @@ -192,6 +199,7 @@ def initialize(args)
@cert_expiration_length_days = DEFAULT_CERT_EXPIRATION_LENGTH_DAYS
@install_extension_in_lib = DEFAULT_INSTALL_EXTENSION_IN_LIB
@ipv4_fallback_enabled = ENV["IPV4_FALLBACK_ENABLED"] == "true" || DEFAULT_IPV4_FALLBACK_ENABLED
@global_gem_cache = ENV["RUBYGEMS_GLOBAL_GEM_CACHE"] == "true" || DEFAULT_GLOBAL_GEM_CACHE

operating_system_config = Marshal.load Marshal.dump(OPERATING_SYSTEM_DEFAULTS)
platform_config = Marshal.load Marshal.dump(PLATFORM_DEFAULTS)
Expand All @@ -213,8 +221,8 @@ def initialize(args)
@hash.transform_keys! do |k|
# gemhome and gempath are not working with symbol keys
if %w[backtrace bulk_threshold verbose update_sources cert_expiration_length_days
install_extension_in_lib ipv4_fallback_enabled sources disable_default_gem_server
ssl_verify_mode ssl_ca_cert ssl_client_cert].include?(k)
install_extension_in_lib ipv4_fallback_enabled global_gem_cache sources
disable_default_gem_server ssl_verify_mode ssl_ca_cert ssl_client_cert].include?(k)
k.to_sym
else
k
Expand All @@ -230,6 +238,7 @@ def initialize(args)
@cert_expiration_length_days = @hash[:cert_expiration_length_days] if @hash.key? :cert_expiration_length_days
@install_extension_in_lib = @hash[:install_extension_in_lib] if @hash.key? :install_extension_in_lib
@ipv4_fallback_enabled = @hash[:ipv4_fallback_enabled] if @hash.key? :ipv4_fallback_enabled
@global_gem_cache = @hash[:global_gem_cache] if @hash.key? :global_gem_cache

@home = @hash[:gemhome] if @hash.key? :gemhome
@path = @hash[:gempath] if @hash.key? :gempath
Expand Down
9 changes: 9 additions & 0 deletions lib/rubygems/defaults.rb
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ def self.cache_home
@cache_home ||= ENV["XDG_CACHE_HOME"] || File.join(Gem.user_home, ".cache")
end

##
# The path to the global gem cache directory.
# This is used when global_gem_cache is enabled to share .gem files
# across all Ruby installations.

def self.global_gem_cache_path
File.join(cache_home, "gem", "gems")
end

##
# The path to standard location of the user's data directory.

Expand Down
7 changes: 5 additions & 2 deletions lib/rubygems/remote_fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,20 @@ def download_to_cache(dependency)
# always replaced.

def download(spec, source_uri, install_dir = Gem.dir)
gem_file_name = File.basename spec.cache_file

install_cache_dir = File.join install_dir, "cache"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we add code that influences the install_dir variable? Or change install_cache_dir variable to point at the gem cache path? It seems like this is the right place to do it. Then I think we can avoid the copy below.

cache_dir =
if Dir.pwd == install_dir # see fetch_command
if Gem.configuration.global_gem_cache
Gem.global_gem_cache_path
elsif Dir.pwd == install_dir # see fetch_command
install_dir
elsif File.writable?(install_cache_dir) || (File.writable?(install_dir) && !File.exist?(install_cache_dir))
install_cache_dir
else
File.join Gem.user_dir, "cache"
end

gem_file_name = File.basename spec.cache_file
local_gem_path = File.join cache_dir, gem_file_name

require "fileutils"
Expand Down
27 changes: 27 additions & 0 deletions test/rubygems/test_gem_config_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,33 @@ def test_initialize_ipv4_fallback_enabled_env
util_config_file %W[--config-file #{@temp_conf}]

assert_equal true, @cfg.ipv4_fallback_enabled
ensure
ENV.delete("IPV4_FALLBACK_ENABLED")
end

def test_initialize_global_gem_cache_default
util_config_file %W[--config-file #{@temp_conf}]

assert_equal false, @cfg.global_gem_cache
end

def test_initialize_global_gem_cache_env
ENV["RUBYGEMS_GLOBAL_GEM_CACHE"] = "true"
util_config_file %W[--config-file #{@temp_conf}]

assert_equal true, @cfg.global_gem_cache
ensure
ENV.delete("RUBYGEMS_GLOBAL_GEM_CACHE")
end

def test_initialize_global_gem_cache_gemrc
File.open @temp_conf, "w" do |fp|
fp.puts "global_gem_cache: true"
end

util_config_file %W[--config-file #{@temp_conf}]

assert_equal true, @cfg.global_gem_cache
end

def test_initialize_handle_arguments_config_file
Expand Down
68 changes: 68 additions & 0 deletions test/rubygems/test_gem_remote_fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,74 @@ def test_yaml_error_on_size
end
end

def test_download_with_global_gem_cache
# Use a temp directory to safely test global cache behavior
test_cache_dir = File.join(@tempdir, "global_gem_cache_test")

Gem.stub :global_gem_cache_path, test_cache_dir do
Gem.configuration.global_gem_cache = true

# Use the real RemoteFetcher with stubbed fetch_path
fetcher = Gem::RemoteFetcher.fetcher
def fetcher.fetch_path(uri, *rest)
File.binread File.join(@test_gem_dir, "a-1.gem")
end
fetcher.instance_variable_set(:@test_gem_dir, File.dirname(@a1_gem))

# With global cache enabled, gem goes directly to global cache
global_cache_gem = File.join(test_cache_dir, @a1.file_name)
assert_equal global_cache_gem, fetcher.download(@a1, "http://gems.example.com")
assert File.exist?(global_cache_gem), "Gem should be in global cache"
end
ensure
Gem.configuration.global_gem_cache = false
end

def test_download_uses_global_gem_cache
# Use a temp directory to safely test global cache behavior
test_cache_dir = File.join(@tempdir, "global_gem_cache_test")

Gem.stub :global_gem_cache_path, test_cache_dir do
Gem.configuration.global_gem_cache = true

# Pre-populate global cache
FileUtils.mkdir_p test_cache_dir
global_cache_gem = File.join(test_cache_dir, @a1.file_name)
FileUtils.cp @a1_gem, global_cache_gem

fetcher = Gem::RemoteFetcher.fetcher

# Should return global cache path without downloading
result = fetcher.download(@a1, "http://gems.example.com")
assert_equal global_cache_gem, result
end
ensure
Gem.configuration.global_gem_cache = false
end

def test_download_without_global_gem_cache
# Use a temp directory to safely test global cache behavior
test_cache_dir = File.join(@tempdir, "global_gem_cache_test")

Gem.stub :global_gem_cache_path, test_cache_dir do
Gem.configuration.global_gem_cache = false

# Use the real RemoteFetcher with stubbed fetch_path
fetcher = Gem::RemoteFetcher.fetcher
def fetcher.fetch_path(uri, *rest)
File.binread File.join(@test_gem_dir, "a-1.gem")
end
fetcher.instance_variable_set(:@test_gem_dir, File.dirname(@a1_gem))

a1_cache_gem = @a1.cache_file
assert_equal a1_cache_gem, fetcher.download(@a1, "http://gems.example.com")

# Verify gem was NOT copied to global cache
global_cache_gem = File.join(test_cache_dir, @a1.file_name)
refute File.exist?(global_cache_gem), "Gem should not be copied to global cache when disabled"
end
end

private

def assert_error(exception_class = Exception)
Expand Down