@@ -20,6 +20,10 @@ module Bundler
2020 extend self
2121
2222 def patch!
23+
24+ return if @bundler_patched
25+ @bundler_patched = true
26+
2327 # Patch to prevent Bundler to save a .bundle/config file in the root
2428 # of the application
2529 ::Bundler ::Settings . module_exec do
@@ -40,15 +44,15 @@ def self.reset_paths!
4044 end
4145 end
4246
43- # When preparing offline packs or generally when installing gems, bundler wants to have `.gem` files
47+ # When preparing offline packs or generally when installing gems, bundler wants to have `.gem` files
4448 # cached. We ship a default set of gems that inclue all of the unpacked code. During dependency
45- # resolution bundler still wants to ensure`.gem` files exist. This patch updates two paths in bundler where
49+ # resolution bundler still wants to ensure`.gem` files exist. This patch updates two paths in bundler where
4650 # it natively it would *fail* when a `.gem` file is not found. Instead of failing we force the cache to be
4751 # updated with a `.gem` file. This preserves the original patch behavior. There is still an open question of
48- # *how* to potentially update the files we vendor or the way we set up bundler to avoid carrying this patch.
52+ # *how* to potentially update the files we vendor or the way we set up bundler to avoid carrying this patch.
4953 # As of JRuby 9.4.13.0 rubygems (bundler) is at 3.6.3. There have been some releases and changes in bundler code
5054 # since then but it does not seem to have changed the way it handles gem files. Obviously carrying a patch like this
51- # carries a maintenance burden so prioritizing a packaging solution may be
55+ # carries a maintenance burden so prioritizing a packaging solution may be
5256 ::Bundler ::Source ::Rubygems . module_exec do
5357 def fetch_gem_if_possible ( spec , previous_spec = nil )
5458 path = if spec . remote
@@ -75,18 +79,278 @@ def cache(spec, custom_path = nil)
7579 raise InstallError , e . message
7680 end
7781 end
82+
83+ #
84+ # BACKPORT: Fix `--prefer-local` flag (from rubygems/rubygems commits 607a3bf479, 209b93a, 23047a0)
85+ #
86+ # The original implementation of --prefer-local was too naive:
87+ # 1. It didn't pass prefer_local to Package objects
88+ # 2. It returned empty array when no local specs exist (instead of falling back to remote)
89+ # 3. It didn't properly handle default gems
90+ #
91+ # These patches fix:
92+ # - PR #7951: Fix `--prefer-local` flag (propagate to packages, add fallback logic)
93+ # - PR #8412: Fix `--prefer-local` not respecting default gems
94+ # - PR #8484: Fix `bundle install --prefer-local` sometimes installing very old versions
95+ #
96+
97+ # Patch Source base class to add prefer_local! method
98+ ::Bundler ::Source . class_eval do
99+ def prefer_local!
100+ # Base implementation - does nothing, subclasses override
101+ end
102+ end
103+
104+ # Patch Source::Rubygems to track prefer_local state and handle default_specs properly
105+ # Also add support for JRuby bundled gems (gems in vendor/jruby/.../specifications/)
106+ ::Bundler ::Source ::Rubygems . class_eval do
107+ # Add prefer_local! method
108+ def prefer_local!
109+ @prefer_local = true
110+ end
111+
112+ # Return specs from JRuby's bundled gem directory (specifications/, not specifications/default/)
113+ # These are gems that ship with JRuby but aren't "default gems" in the Ruby sense
114+ def jruby_bundled_specs
115+ @jruby_bundled_specs ||= begin
116+ idx = ::Bundler ::Index . new
117+ jruby_specs_dir = LogStash ::Bundler . instance_variable_get ( :@jruby_bundled_specs_dir )
118+ jruby_default_specs_dir = LogStash ::Bundler . instance_variable_get ( :@jruby_default_specs_dir )
119+
120+ if jruby_specs_dir && ::File . directory? ( jruby_specs_dir )
121+ # Get gemspecs from specifications/ but NOT from specifications/default/
122+ ::Dir [ ::File . join ( jruby_specs_dir , "*.gemspec" ) ] . each do |path |
123+ # Skip if this is actually in the default directory
124+ next if jruby_default_specs_dir && path . start_with? ( jruby_default_specs_dir )
125+
126+ stub = ::Gem ::StubSpecification . gemspec_stub ( path , jruby_specs_dir , jruby_specs_dir )
127+ # Create a Bundler::StubSpecification from the Gem::StubSpecification
128+ bundler_spec = ::Bundler ::StubSpecification . from_stub ( stub )
129+ # Set source to self (the Source::Rubygems instance) - required for materialization
130+ bundler_spec . source = self
131+ idx << bundler_spec
132+ end
133+ end
134+ idx
135+ end
136+ end
137+
138+ # Override specs method to handle prefer_local for default_specs AND jruby_bundled_specs
139+ alias_method :original_specs , :specs
140+
141+ def specs
142+ @specs ||= begin
143+ # remote_specs usually generates a way larger Index than the other
144+ # sources, and large_idx.merge! small_idx is way faster than
145+ # small_idx.merge! large_idx.
146+ index = @allow_remote ? remote_specs . dup : ::Bundler ::Index . new
147+ index . merge! ( cached_specs ) if @allow_cached
148+ index . merge! ( installed_specs ) if @allow_local
149+
150+ if @allow_local
151+ if @prefer_local
152+ # With prefer_local, merge jruby_bundled_specs and default_specs so they take precedence
153+ # over remote/cached/installed specs. This ensures JRuby's bundled gems are preferred.
154+ index . merge! ( jruby_bundled_specs )
155+ index . merge! ( default_specs )
156+ else
157+ # complete with default specs, only if not already available in the
158+ # index through remote, cached, or installed specs
159+ index . use ( jruby_bundled_specs )
160+ index . use ( default_specs )
161+ end
162+ end
163+
164+ index
165+ end
166+ end
167+ end
168+
169+ # Patch SourceList to propagate prefer_local! to all sources
170+ ::Bundler ::SourceList . class_eval do
171+ def prefer_local!
172+ all_sources . each ( &:prefer_local! )
173+ end
174+ end
175+
176+ # Patch Definition to call sources.prefer_local! when prefer_local! is called
177+ ::Bundler ::Definition . class_eval do
178+ alias_method :original_prefer_local! , :prefer_local!
179+
180+ def prefer_local!
181+ @prefer_local = true
182+ sources . prefer_local!
183+ end
184+ end
185+
186+ # Patch Package to add prefer_local support
187+ ::Bundler ::Resolver ::Package . class_eval do
188+ def prefer_local?
189+ @prefer_local
190+ end
191+
192+ def consider_remote_versions!
193+ @prefer_local = false
194+ end
195+ end
196+
197+ # Patch Resolver::Base to propagate prefer_local to packages and add include_remote_specs
198+ ::Bundler ::Resolver ::Base . class_eval do
199+ alias_method :original_base_initialize , :initialize
200+
201+ def initialize ( source_requirements , dependencies , base , platforms , options )
202+ @prefer_local_option = options [ :prefer_local ]
203+ original_base_initialize ( source_requirements , dependencies , base , platforms , options )
204+ end
205+
206+ alias_method :original_get_package , :get_package
207+
208+ def get_package ( name )
209+ package = original_get_package ( name )
210+ # Inject prefer_local into packages since older Bundler doesn't pass it through
211+ if @prefer_local_option && !package . instance_variable_get ( :@prefer_local )
212+ package . instance_variable_set ( :@prefer_local , true )
213+ end
214+ package
215+ end
216+
217+ def include_remote_specs ( names )
218+ names . each do |name |
219+ get_package ( name ) . consider_remote_versions!
220+ end
221+ end
222+ end
223+
224+ # Patch Resolver to fix filter_remote_specs with proper fallback
225+ ::Bundler ::Resolver . class_eval do
226+ # Override filter_remote_specs with the fixed version from Bundler 2.7+
227+ # This fixes the issue where --prefer-local would return empty specs
228+ # when no local gems are installed, instead of falling back to remote
229+ def filter_remote_specs ( specs , package )
230+ if package . prefer_local?
231+ local_specs = specs . select { |s | s . is_a? ( ::Bundler ::StubSpecification ) }
232+
233+ if local_specs . empty?
234+ # BACKPORT FIX: If no local specs exist, fall back to remote specs
235+ # instead of returning empty array
236+ package . consider_remote_versions!
237+ specs
238+ else
239+ local_specs
240+ end
241+ else
242+ specs
243+ end
244+ end
245+ end
246+
247+ # Patch Source::Rubygems#install to skip installation for default gems and JRuby bundled gems
248+ # The original condition `spec.default_gem? && !cached_built_in_gem(...)` has a side effect:
249+ # cached_built_in_gem fetches from remote if not in cache. For default gems and JRuby bundled gems,
250+ # we should skip installation entirely without needing a cached .gem file.
251+ ::Bundler ::Source ::Rubygems . class_eval do
252+ alias_method :original_rubygems_install , :install
253+
254+ def install ( spec , options = { } )
255+ # For default gems, skip installation entirely - they're already available
256+ if spec . default_gem?
257+ print_using_message "Using #{ version_message ( spec , options [ :previous_spec ] ) } "
258+ return nil
259+ end
260+
261+ # For JRuby bundled gems, also skip installation - they're already available
262+ # Check if this exact gem (name + version) exists in JRuby's bundled gems
263+ jruby_specs_dir = LogStash ::Bundler . instance_variable_get ( :@jruby_bundled_specs_dir )
264+ if jruby_specs_dir && ::File . directory? ( jruby_specs_dir )
265+ jruby_gemspec_path = ::File . join ( jruby_specs_dir , "#{ spec . name } -#{ spec . version } .gemspec" )
266+ if ::File . exist? ( jruby_gemspec_path )
267+ print_using_message "Using #{ version_message ( spec , options [ :previous_spec ] ) } "
268+ return nil
269+ end
270+ end
271+
272+ original_rubygems_install ( spec , options )
273+ end
274+ end
78275 end
79276
80277
278+ # Capture JRuby's default gem directory before paths are changed
279+ # This is needed so that default gems (like json) can be found with --prefer-local
280+ def preserve_jruby_default_gems_path
281+ return @jruby_default_gem_dir if defined? ( @jruby_default_gem_dir )
282+
283+ # The Gradle/JRuby setup already changes Gem.default_dir to a temp path before
284+ # this code runs, so we need to construct the actual JRuby path from LOGSTASH_HOME
285+ logstash_home = ENV [ "LOGSTASH_HOME" ] || ::File . expand_path ( "../../.." , __FILE__ )
286+ jruby_gems_dir = ::File . join ( logstash_home , "vendor" , "jruby" , "lib" , "ruby" , "gems" , "shared" )
287+ jruby_default_specs = ::File . join ( jruby_gems_dir , "specifications" , "default" )
288+ jruby_bundled_specs = ::File . join ( jruby_gems_dir , "specifications" )
289+
290+ if ::File . directory? ( jruby_default_specs )
291+ @jruby_default_gem_dir = jruby_gems_dir
292+ @jruby_default_specs_dir = jruby_default_specs
293+ @jruby_bundled_specs_dir = jruby_bundled_specs
294+ else
295+ # Fall back to Gem.default_dir if vendor/jruby doesn't exist
296+ @jruby_default_gem_dir = ::Gem . default_dir
297+ @jruby_default_specs_dir = ::Gem . default_specifications_dir
298+ @jruby_bundled_specs_dir = nil
299+ end
300+
301+ @jruby_default_gem_dir
302+ end
303+
304+ # Patch Gem::Specification.default_stubs to also look in JRuby's original default specs directory
305+ # This is needed because Gem.default_specifications_dir only returns a single path,
306+ # and after Gem.paths = ENV it points to Logstash's gem home, not JRuby's installation
307+ def patch_default_stubs!
308+ return if @default_stubs_patched || !defined? ( @jruby_default_specs_dir ) || @jruby_default_specs_dir . nil?
309+ @default_stubs_patched = true
310+
311+ jruby_specs_dir = @jruby_default_specs_dir
312+
313+ ::Gem ::Specification . singleton_class . class_eval do
314+ alias_method :original_default_stubs , :default_stubs
315+
316+ define_method ( :default_stubs ) do |pattern = "*.gemspec" |
317+ # Get stubs from the current default_specifications_dir
318+ stubs = original_default_stubs ( pattern )
319+
320+ # Also look in JRuby's original default specs directory if it exists and is different
321+ if jruby_specs_dir && ::File . directory? ( jruby_specs_dir ) && jruby_specs_dir != ::Gem . default_specifications_dir
322+ base_dir = jruby_specs_dir
323+ ::Dir [ ::File . join ( base_dir , pattern ) ] . each do |path |
324+ # Use default_gemspec_stub to mark these as default gems (default_gem = true)
325+ stub = ::Gem ::StubSpecification . default_gemspec_stub ( path , base_dir , base_dir )
326+ stubs << stub unless stubs . any? { |s | s . name == stub . name && s . version == stub . version }
327+ end
328+ end
329+
330+ stubs
331+ end
332+ end
333+ end
334+
81335 # prepare bundler's environment variables, but do not invoke ::Bundler::setup
82336 def prepare ( options = { } )
83337 options = { :without => [ :development ] } . merge ( options )
84338 options [ :without ] = Array ( options [ :without ] )
85339
340+ # Capture JRuby default gems path BEFORE clearing
341+ jruby_gem_dir = preserve_jruby_default_gems_path
342+
86343 ::Gem . clear_paths
87- ENV [ 'GEM_HOME' ] = ENV [ 'GEM_PATH' ] = Environment . logstash_gem_home
344+ # Include both Logstash gem home AND JRuby's default gem directory in GEM_PATH
345+ # This ensures default gems can be discovered by Gem::Specification.default_stubs
346+ gem_path = [ Environment . logstash_gem_home , jruby_gem_dir ] . compact . uniq . join ( ::File ::PATH_SEPARATOR )
347+ ENV [ 'GEM_HOME' ] = Environment . logstash_gem_home
348+ ENV [ 'GEM_PATH' ] = gem_path
88349 ::Gem . paths = ENV
89350
351+ # Patch default_stubs to also look in JRuby's original location
352+ patch_default_stubs!
353+
90354 # set BUNDLE_GEMFILE ENV before requiring bundler to avoid bundler recurse and load unrelated Gemfile(s)
91355 ENV [ "BUNDLE_GEMFILE" ] = Environment ::GEMFILE_PATH
92356
@@ -127,9 +391,21 @@ def invoke!(options = {})
127391 :jobs => 12 , :all => false , :package => false , :without => [ :development ] } . merge ( options )
128392 options [ :without ] = Array ( options [ :without ] )
129393 options [ :update ] = Array ( options [ :update ] ) if options [ :update ]
394+
395+ # Capture JRuby default gems path BEFORE clearing
396+ jruby_gem_dir = preserve_jruby_default_gems_path
397+
130398 ::Gem . clear_paths
131- ENV [ 'GEM_HOME' ] = ENV [ 'GEM_PATH' ] = LogStash ::Environment . logstash_gem_home
399+ # Include both Logstash gem home AND JRuby's default gem directory in GEM_PATH
400+ # This ensures default gems can be discovered by Gem::Specification.default_stubs
401+ gem_path = [ LogStash ::Environment . logstash_gem_home , jruby_gem_dir ] . compact . uniq . join ( ::File ::PATH_SEPARATOR )
402+ ENV [ 'GEM_HOME' ] = LogStash ::Environment . logstash_gem_home
403+ ENV [ 'GEM_PATH' ] = gem_path
132404 ::Gem . paths = ENV
405+
406+ # Patch default_stubs to also look in JRuby's original location
407+ patch_default_stubs!
408+
133409 # set BUNDLE_GEMFILE ENV before requiring bundler to avoid bundler recurse and load unrelated Gemfile(s).
134410 # in the context of calling Bundler::CLI this is not really required since Bundler::CLI will look at
135411 # Bundler.settings[:gemfile] unlike Bundler.setup. For the sake of consistency and defensive/future proofing, let's keep it here.
0 commit comments