From 0e4dbb2d5d3ad64017924bd482454a59d8da6508 Mon Sep 17 00:00:00 2001 From: Kazuhiro NISHIYAMA Date: Wed, 24 Jun 2026 09:25:45 +0900 Subject: [PATCH 01/11] [DOC] Fix the return value of Ractor#monitor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The RDoc `call-seq` for `Ractor#monitor` says it returns `self`, but the method actually returns `true`/`false`. It is implemented by `ractor_monitor()` in `ractor_sync.c`, which returns only `Qtrue` or `Qfalse` — there is no code path that returns `self`: - `true` — the monitor was registered (the ractor is still running) - `false` — the ractor had already terminated, so it was not registered; the termination message (`:exited` / `:aborted`) is sent to the port immediately (For comparison, `Ractor#unmonitor` does `return self`, so its existing `-> self` call-seq is correct and is left unchanged.) This patch corrects the `call-seq` to `-> true or false` and documents the meaning of the two return values. --- ractor.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ractor.rb b/ractor.rb index 2dc60f5ff64926..366a1192c818d2 100644 --- a/ractor.rb +++ b/ractor.rb @@ -615,7 +615,7 @@ def value # # call-seq: - # ractor.monitor(port) -> self + # ractor.monitor(port) -> true or false # # Registers the port as a monitoring port for this ractor. When the ractor terminates, # the port receives a Symbol object. @@ -623,6 +623,10 @@ def value # * +:exited+ is sent if the ractor terminates without an unhandled exception. # * +:aborted+ is sent if the ractor terminates by an unhandled exception. # + # Returns +true+ if the monitor was registered (the ractor is still running). + # Returns +false+ if the ractor had already terminated; in that case the + # termination message (+:exited+ or +:aborted+) is sent to the port immediately. + # # r = Ractor.new{ some_task() } # r.monitor(port = Ractor::Port.new) # port.receive #=> :exited and r is terminated From 17187e40fa8f3eb9307106824f10587c2b0f25ae Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 24 Jun 2026 11:20:16 +0900 Subject: [PATCH 02/11] [ruby/rubygems] Skip PQC connection tests when the runtime cannot negotiate PQC OpenSSL >= 3.5 can ship ML-KEM and ML-DSA while still keeping them out of the default negotiation lists, for example under RHEL's system-wide crypto policies. The tests force a PQC-only server but connect with the default gem fetcher, so they fail the handshake on such hosts even though the version gate passes. Probe a real loopback handshake instead. https://github.com/ruby/rubygems/commit/ca011a9897 Co-Authored-By: Claude Opus 4.8 --- ...est_gem_remote_fetcher_local_ssl_server.rb | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/rubygems/test_gem_remote_fetcher_local_ssl_server.rb b/test/rubygems/test_gem_remote_fetcher_local_ssl_server.rb index 129c216259e63a..780846d0156b1a 100644 --- a/test/rubygems/test_gem_remote_fetcher_local_ssl_server.rb +++ b/test/rubygems/test_gem_remote_fetcher_local_ssl_server.rb @@ -231,5 +231,54 @@ def omit_unless_support_pqc # mode :pqc requires Ruby OpenSSL >= 4.0. omit "PQC test requires Ruby OpenSSL >= 4.0" unless Gem::Version.new(OpenSSL::VERSION) >= Gem::Version.new("4.0") + # Even with a new enough OpenSSL, the runtime may keep PQC groups and + # signature algorithms out of its default negotiation lists (for example + # RHEL's system-wide crypto policies). The PQC server forces both, while + # the gem fetcher connects with the default client configuration, so a + # real loopback handshake is the only reliable way to tell whether this + # environment can negotiate PQC at all. + omit "PQC handshake is not available in this OpenSSL configuration" unless + self.class.support_pqc_handshake? + end + + # Probe an actual PQC handshake between a forced-PQC server and a + # default-configured client, mirroring what the integration tests exercise. + # Memoized so the probe runs at most once per process. + def self.support_pqc_handshake? + return @support_pqc_handshake unless @support_pqc_handshake.nil? + + @support_pqc_handshake = probe_pqc_handshake + end + + def self.probe_pqc_handshake + server = TCPServer.new("127.0.0.1", 0) + ctx = OpenSSL::SSL::SSLContext.new + ctx.cert = OpenSSL::X509::Certificate.new(File.read(File.join(__dir__, "mldsa65_ssl_cert.pem"))) + ctx.key = OpenSSL::PKey.read(File.read(File.join(__dir__, "mldsa65_ssl_key.pem"))) + ctx.groups = "X25519MLKEM768" + ssl_server = OpenSSL::SSL::SSLServer.new(server, ctx) + + port = server.addr[1] + server_thread = Thread.new do + client = ssl_server.accept + client.close + rescue OpenSSL::OpenSSLError + nil + end + + client_ctx = OpenSSL::SSL::SSLContext.new + client_ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE + socket = TCPSocket.new("127.0.0.1", port) + ssl = OpenSSL::SSL::SSLSocket.new(socket, client_ctx) + ssl.connect + ssl.close + true + rescue OpenSSL::OpenSSLError, SystemCallError + false + ensure + server_thread&.join(5) + server_thread&.kill if server_thread&.alive? + ssl_server&.close + server&.close end end if Gem::HAVE_OPENSSL From 1f473ed633a364f8c89b69a76adfb22a4bf21f96 Mon Sep 17 00:00:00 2001 From: viralpraxis Date: Tue, 23 Jun 2026 18:14:46 +0300 Subject: [PATCH 03/11] [Bug #22126] Fix partial DCE with loop back-edges The old `remove_unreachable_chunk` walked the dead code once and tried to do two things at the same time -- count label references inside that chunk and figure out what exactly to delete. That breaks for loops -- the loop label comes before the jump back to it; on the first walk, the label looks like it has no references inside the chunk yet, so the optimizer thinks something outside the dead code still needs it. It stops there and deletes only the setup code before the loop, leaving the loop body behind. I've changed the algorithm to use two passes to mitigate the issue; I believe the performance overhead is neglible ```shell $ ruby --parser parse.y -e 'if []; else; a => [*, 42, *]; end' $ ruby --parser prism -e 'if []; else; a => [*, 42, *]; end' -- raw disasm-------- trace: 1 0000 putnil ( 1) 0001 leave ( 1) [sp: 1, unremovable: 0, refcnt: 1] 0002 dup ( 1) 0003 topn 2 ( 1) 0005 opt_le ( 1) 0007 branchunless ( 1) 0009 topn 3 ( 1) 0011 topn 1 ( 1) 0013 opt_aref ( 1) 0015 putobject 42 ( 1) 0017 dupn 2 ( 1) 0019 checkmatch 2 ( 1) 0021 dup ( 1) 0022 branchif ( 1) 0024 putspecialobject 1 ( 1) 0026 putobject "%p === %p does not return true" ( 1) 0028 topn 3 ( 1) 0030 topn 5 ( 1) 0032 opt_send_without_block ( 1) 0034 setn 10 ( 1) 0036 putobject false ( 1) 0038 setn 12 ( 1) 0040 pop ( 1) 0041 pop ( 1) [sp: 4, unremovable: 0, refcnt: 1] 0042 setn 2 ( 1) 0044 pop ( 1) 0045 pop ( 1) 0046 branchif ( 1) 0048 putobject_INT2FIX_1_ ( 1) 0049 opt_plus ( 1) 0051 jump ( 1) [sp: 1, unremovable: 0, refcnt: 1] * 0053 adjuststack 3 ( 1) 0055 putspecialobject 1 ( 1) 0057 putobject "%p does not match to find pattern" ( 1) 0059 topn 2 ( 1) 0061 opt_send_without_block ( 1) 0063 setn 4 ( 1) 0065 putobject false ( 1) 0067 setn 6 ( 1) 0069 pop ( 1) 0070 pop ( 1) 0071 jump ( 1) [sp: 1, unremovable: 0, refcnt: 1] 0073 adjuststack 3 ( 1) 0075 pop ( 1) 0076 jump ( 1) [sp: -1, unremovable: 0, refcnt: 1] 0078 pop ( 1) 0079 putspecialobject 1 ( 1) 0081 topn 4 ( 1) 0083 branchif ( 1) 0085 putobject NoMatchingPatternError ( 1) 0087 putspecialobject 1 ( 1) 0089 putobject "%p: %s" ( 1) 0091 topn 4 ( 1) 0093 topn 7 ( 1) 0095 opt_send_without_block ( 1) 0097 opt_send_without_block ( 1) 0099 jump ( 1) [sp: -1, unremovable: 0, refcnt: 1] 0101 putobject NoMatchingPatternKeyError ( 1) 0103 putspecialobject 1 ( 1) 0105 putobject "%p: %s" ( 1) 0107 topn 4 ( 1) 0109 topn 7 ( 1) 0111 opt_send_without_block ( 1) 0113 topn 7 ( 1) 0115 topn 9 ( 1) 0117 opt_send_without_block ( 1) 0119 opt_send_without_block ( 1) [sp: -1, unremovable: 0, refcnt: 1] 0121 adjuststack 7 ( 1) 0123 putnil ( 1) 0124 leave ( 1) [sp: -1, unremovable: 0, refcnt: 1] 0125 adjuststack 6 ( 1) 0127 putnil ( 1) 0128 leave ( 1) --------------------- -e:1: argument stack underflow (-2) -e: compile error (SyntaxError) ``` Reproducible on every version since `3.4.0-preview2` (`docker run -e ALL_RUBY_SHOW_DUP=yes -e ALL_RUBY_SINCE=3.4 --rm rubylang/all-ruby ./all-ruby -We 'if []; else; a => [*, 42, *]; end'`) There seem to be another issue related to Prism's DCE & loops: ```shell $ ruby --parser parse.y --dump insn -e 'if []; else; while true; end; end' == disasm: #@-e:1 (1,0)-(1,33)> 0000 putnil ( 1)[Li] 0001 leave $ ruby --parser prism --dump insn -e 'if []; else; while true; end; end' == disasm: #@-e:1 (1,0)-(1,33)> 0000 newarray 0 ( 1)[Li] 0002 branchunless 11 0004 putnil 0005 leave 0006 pop 0007 jump 11 0009 putnil 0010 pop 0011 jump 11 0013 putnil 0014 leave ``` the reason `branchunless` is not eliminated is that prism does not emit `NODE_ZLIST` like `parse.y` does and instead emits `PM_ARRAY_NODE-0` which is not treated as a always-truthy value by the peephole-optmizer. --- compile.c | 45 ++++++++++++++++++++++++++++-------------- test/ruby/test_iseq.rb | 4 ++++ 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/compile.c b/compile.c index cbf00c9da113b8..4a0a5e83f3a3c0 100644 --- a/compile.c +++ b/compile.c @@ -3149,41 +3149,56 @@ find_destination(INSN *i) static int remove_unreachable_chunk(rb_iseq_t *iseq, LINK_ELEMENT *i) { - LINK_ELEMENT *first = i, *end; + LINK_ELEMENT *first = i, *end, *scan; int *unref_counts = 0, nlabels = ISEQ_COMPILE_DATA(iseq)->label_no; if (!i) return 0; unref_counts = ALLOCA_N(int, nlabels); MEMZERO(unref_counts, int, nlabels); - end = i; + + scan = i; do { LABEL *lab; - if (IS_INSN(i)) { - if (IS_INSN_ID(i, leave)) { - end = i; + if (IS_INSN(scan)) { + if (IS_INSN_ID(scan, leave)) { break; } - else if ((lab = find_destination((INSN *)i)) != 0) { + else if ((lab = find_destination((INSN *)scan)) != 0) { unref_counts[lab->label_no]++; } } - else if (IS_LABEL(i)) { - lab = (LABEL *)i; + else if (IS_LABEL(scan)) { + lab = (LABEL *)scan; if (lab->unremovable) return 0; + } + else if (IS_ADJUST(scan)) { + return 0; + } + } while ((scan = scan->next) != 0); + + end = i; + scan = i; + do { + LABEL *lab; + if (IS_INSN(scan)) { + if (IS_INSN_ID(scan, leave)) { + end = scan; + break; + } + } + else if (IS_LABEL(scan)) { + lab = (LABEL *)scan; if (lab->refcnt > unref_counts[lab->label_no]) { - if (i == first) return 0; + if (scan == first) return 0; break; } continue; } - else if (IS_TRACE(i)) { - /* do nothing */ - } - else if (IS_ADJUST(i)) { + else if (IS_ADJUST(scan)) { return 0; } - end = i; - } while ((i = i->next) != 0); + end = scan; + } while ((scan = scan->next) != 0); i = first; do { if (IS_INSN(i)) { diff --git a/test/ruby/test_iseq.rb b/test/ruby/test_iseq.rb index b4760dc412779c..30931587f8b6e4 100644 --- a/test/ruby/test_iseq.rb +++ b/test/ruby/test_iseq.rb @@ -953,6 +953,10 @@ def test_unreachable_next_in_block end end + def test_unreachable_find_pattern_in_else_branch + assert_in_out_err([], "if []; else; a => [*, 42, *]; end") + end + def test_serialize_anonymous_outer_variables iseq = RubyVM::InstructionSequence.compile(<<~'RUBY') obj = Object.new From 3bd6565a3841e1256dca62736ff60e27d68b5375 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 22 Jun 2026 19:46:48 +0900 Subject: [PATCH 04/11] [ruby/rubygems] Move Bundler runtime tree to the top level Flatten bundler/lib, bundler/exe and bundler/bundler.gemspec into the repository's top-level lib/, exe/ and bundler.gemspec to converge on the flat layout ruby/ruby already uses. Bundler is fully require_relative based, so the source move is mechanical and updates only the load-path references in .rspec, .rubocop.yml, Rakefile, the spec path helper and the CI workflows. The Bundler docs stay under bundler/ for now and move in a later commit. https://github.com/ruby/rubygems/commit/916373ab9e --- spec/bundler/support/path.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/bundler/support/path.rb b/spec/bundler/support/path.rb index 4b80c5b4a444d9..550a59408869a3 100644 --- a/spec/bundler/support/path.rb +++ b/spec/bundler/support/path.rb @@ -10,7 +10,7 @@ module Path include Spec::Env def source_root - @source_root ||= Pathname.new(ruby_core? ? "../../.." : "../../bundler").expand_path(__dir__) + @source_root ||= Pathname.new(ruby_core? ? "../../.." : "../..").expand_path(__dir__) end def root @@ -50,7 +50,7 @@ def dev_binstub end def bindir - @bindir ||= source_root.join(ruby_core? ? "spec/bin" : "../bin") + @bindir ||= source_root.join(ruby_core? ? "spec/bin" : "bin") end def exedir @@ -76,7 +76,7 @@ def path end def spec_dir - @spec_dir ||= source_root.join(ruby_core? ? "spec/bundler" : "../spec") + @spec_dir ||= source_root.join(ruby_core? ? "spec/bundler" : "spec") end def man_dir @@ -123,7 +123,7 @@ def tmp_root end Pathname(real) else - (ruby_core? ? source_root : source_root.parent).join("tmp") + source_root.join("tmp") end end @@ -298,7 +298,7 @@ def replace_changelog(version, dir:) end def git_root - ruby_core? ? source_root : source_root.parent + source_root end def rake_path @@ -345,11 +345,11 @@ def git_ls_files(glob) end def tracked_files_glob - ruby_core? ? "libexec/bundle* lib/bundler lib/bundler.rb spec/bundler man/bundle*" : "lib exe CHANGELOG.md LICENSE.md README.md bundler.gemspec" + ruby_core? ? "libexec/bundle* lib/bundler lib/bundler.rb spec/bundler man/bundle*" : "exe/bundle exe/bundler lib/bundler lib/bundler.rb bundler.gemspec bundler/CHANGELOG.md bundler/LICENSE.md bundler/README.md" end def lib_tracked_files_glob - ruby_core? ? "lib/bundler lib/bundler.rb" : "lib" + "lib/bundler lib/bundler.rb" end def man_tracked_files_glob @@ -371,7 +371,7 @@ def standard_gemfile_basename end def tool_dir - ruby_core? ? source_root.join("tool/bundler") : source_root.join("../tool/bundler") + source_root.join("tool/bundler") end def templates_dir From 2b74a0b4a5a275e40794a705836a7a4ae160e0cf Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 22 Jun 2026 19:48:42 +0900 Subject: [PATCH 05/11] [ruby/rubygems] Update `gem update --system` for the flat Bundler layout setup_command builds and installs the default Bundler gem from the source tree. With Bundler flattened into the top-level lib/ and bundler.gemspec, the separate "bundler/lib" library and the chdir into bundler/ are gone: the single lib/ install now carries both RubyGems and Bundler, and the gem is built straight from the top-level gemspec. https://github.com/ruby/rubygems/commit/3585865d3c --- lib/rubygems/commands/setup_command.rb | 43 +++++++++---------- test/rubygems/bundler_test_gem.rb | 2 +- .../test_gem_commands_setup_command.rb | 36 ++++++++++------ 3 files changed, 44 insertions(+), 37 deletions(-) diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb index 175599967cf62d..ad4aaab384a0c6 100644 --- a/lib/rubygems/commands/setup_command.rb +++ b/lib/rubygems/commands/setup_command.rb @@ -292,7 +292,6 @@ def shebang def install_lib(lib_dir) libs = { "RubyGems" => "lib" } - libs["Bundler"] = "bundler/lib" libs.each do |tool, path| say "Installing #{tool}" if @verbose @@ -366,7 +365,7 @@ def install_default_bundler_gem(bin_dir) target_specs_dir end - new_bundler_spec = Dir.chdir("bundler") { Gem::Specification.load("bundler.gemspec") } + new_bundler_spec = Gem::Specification.load("bundler.gemspec") full_name = new_bundler_spec.full_name gemspec_path = "#{full_name}.gemspec" @@ -390,26 +389,24 @@ def install_default_bundler_gem(bin_dir) require_relative "../installer" - Dir.chdir("bundler") do - built_gem = Gem::Package.build(new_bundler_spec) - begin - installer = Gem::Installer.at( - built_gem, - env_shebang: options[:env_shebang], - format_executable: options[:format_executable], - force: options[:force], - bin_dir: bin_dir, - install_dir: default_dir, - wrappers: true - ) - # We need to install only executable and default spec files. - # lib/bundler.rb and lib/bundler/* are available under the site_ruby directory. - installer.extract_bin - installer.generate_bin - installer.write_default_spec - ensure - FileUtils.rm_f built_gem - end + built_gem = Gem::Package.build(new_bundler_spec) + begin + installer = Gem::Installer.at( + built_gem, + env_shebang: options[:env_shebang], + format_executable: options[:format_executable], + force: options[:force], + bin_dir: bin_dir, + install_dir: default_dir, + wrappers: true + ) + # We need to install only executable and default spec files. + # lib/bundler.rb and lib/bundler/* are available under the site_ruby directory. + installer.extract_bin + installer.generate_bin + installer.write_default_spec + ensure + FileUtils.rm_f built_gem end new_bundler_spec.executables.each {|executable| bin_file_names << target_bin_path(bin_dir, executable) } @@ -499,7 +496,7 @@ def remove_old_bin_files(bin_dir) def remove_old_lib_files(lib_dir) lib_dirs = { File.join(lib_dir, "rubygems") => "lib/rubygems" } - lib_dirs[File.join(lib_dir, "bundler")] = "bundler/lib/bundler" + lib_dirs[File.join(lib_dir, "bundler")] = "lib/bundler" lib_dirs.each do |old_lib_dir, new_lib_dir| lib_files = files_in(new_lib_dir) diff --git a/test/rubygems/bundler_test_gem.rb b/test/rubygems/bundler_test_gem.rb index ca2980e04b519a..1f356bb42c3059 100644 --- a/test/rubygems/bundler_test_gem.rb +++ b/test/rubygems/bundler_test_gem.rb @@ -408,7 +408,7 @@ def with_local_bundler_at(path) require "bundler" # If bundler gemspec exists, pretend it's installed - bundler_gemspec = File.expand_path("../../bundler/bundler.gemspec", __dir__) + bundler_gemspec = File.expand_path("../../bundler.gemspec", __dir__) if File.exist?(bundler_gemspec) target_gemspec_location = "#{path}/specifications/bundler-#{Bundler::VERSION}.gemspec" diff --git a/test/rubygems/test_gem_commands_setup_command.rb b/test/rubygems/test_gem_commands_setup_command.rb index b33e05ab28dc55..7df59ecfc6dbbb 100644 --- a/test/rubygems/test_gem_commands_setup_command.rb +++ b/test/rubygems/test_gem_commands_setup_command.rb @@ -15,15 +15,15 @@ def setup lib/rubygems.rb lib/rubygems/requirement.rb lib/rubygems/ssl_certs/rubygems.org/foo.pem - bundler/exe/bundle - bundler/exe/bundler - bundler/lib/bundler.rb - bundler/lib/bundler/b.rb - bundler/bin/bundler/man/bundle-b.1 - bundler/lib/bundler/man/bundle-b.1.ronn - bundler/lib/bundler/man/gemfile.5 - bundler/lib/bundler/man/gemfile.5.ronn - bundler/lib/bundler/templates/.circleci/config.yml + exe/bundle + exe/bundler + lib/bundler.rb + lib/bundler/b.rb + lib/bundler/man/bundle-b.1 + lib/bundler/man/bundle-b.1.ronn + lib/bundler/man/gemfile.5 + lib/bundler/man/gemfile.5.ronn + lib/bundler/templates/.circleci/config.yml ] create_dummy_files(filelist) @@ -34,7 +34,7 @@ def setup s.files = ["lib/bundler.rb"] end - File.open "bundler/bundler.gemspec", "w" do |io| + File.open "bundler.gemspec", "w" do |io| io.puts gemspec.to_ruby end @@ -171,8 +171,18 @@ def test_destdir_flag_regenerates_binstubs end def test_files_in - assert_equal %w[rubygems.rb rubygems/requirement.rb rubygems/ssl_certs/rubygems.org/foo.pem], - @cmd.files_in("lib").sort + assert_equal %w[ + bundler.rb + bundler/b.rb + bundler/man/bundle-b.1 + bundler/man/bundle-b.1.ronn + bundler/man/gemfile.5 + bundler/man/gemfile.5.ronn + bundler/templates/.circleci/config.yml + rubygems.rb + rubygems/requirement.rb + rubygems/ssl_certs/rubygems.org/foo.pem + ], @cmd.files_in("lib").sort end def test_install_lib @@ -475,7 +485,7 @@ def new_bundler_specification_path end def bundler_spec - Gem::Specification.load("bundler/bundler.gemspec") + Gem::Specification.load("bundler.gemspec") end def bundler_version From 4a5413752d68eb59035796c135aa9204a4e59252 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 22 Jun 2026 19:50:35 +0900 Subject: [PATCH 06/11] [ruby/rubygems] Move Bundler docs to the top level under aliased names Move bundler/{CHANGELOG,LICENSE,README}.md to the top level as CHANGELOG-bundler.md, LICENSE-bundler.md and README-bundler.md so they no longer collide with the RubyGems documents while keeping the CHANGELOG*/LICENSE*/README* prefixes that license scanners rely on. bundler/.document is dropped since lib/bundler/.document already excludes the runtime tree from RDoc, leaving bundler/ empty. The single-document direction stays for a later docs merge; this only de-collides the file names. https://github.com/ruby/rubygems/commit/db533d1651 --- lib/bundler/bundler.gemspec | 7 +++++-- spec/bundler/support/build_metadata.rb | 2 +- spec/bundler/support/path.rb | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/bundler/bundler.gemspec b/lib/bundler/bundler.gemspec index 49319e81b4f08c..77e957108ccf6c 100644 --- a/lib/bundler/bundler.gemspec +++ b/lib/bundler/bundler.gemspec @@ -24,9 +24,9 @@ Gem::Specification.new do |s| s.metadata = { "bug_tracker_uri" => "https://github.com/ruby/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3ABundler", - "changelog_uri" => "https://github.com/ruby/rubygems/blob/master/bundler/CHANGELOG.md", + "changelog_uri" => "https://github.com/ruby/rubygems/blob/master/CHANGELOG-bundler.md", "homepage_uri" => "https://bundler.io/", - "source_code_uri" => "https://github.com/ruby/rubygems/tree/master/bundler", + "source_code_uri" => "https://github.com/ruby/rubygems", } s.required_ruby_version = ">= 3.2.0" @@ -39,6 +39,9 @@ Gem::Specification.new do |s| # include the gemspec itself because warbler breaks w/o it s.files += %w[lib/bundler/bundler.gemspec] + # These live next to the gemspec when Bundler ships as a gem, but not when + # it is synced into Ruby core, where the gemspec moves under lib/bundler. + s.files += %w[CHANGELOG-bundler.md LICENSE-bundler.md README-bundler.md].select {|f| File.file?(f) } s.bindir = "exe" s.executables = %w[bundle bundler] s.require_paths = ["lib"] diff --git a/spec/bundler/support/build_metadata.rb b/spec/bundler/support/build_metadata.rb index 2eade4137bd68f..8469d96a760112 100644 --- a/spec/bundler/support/build_metadata.rb +++ b/spec/bundler/support/build_metadata.rb @@ -44,7 +44,7 @@ def git_commit_sha end def release_date_for(version, dir:) - changelog = File.expand_path("CHANGELOG.md", dir) + changelog = File.expand_path("CHANGELOG-bundler.md", dir) File.readlines(changelog)[2].scan(/^## #{Regexp.escape(version)} \((.*)\)/).first&.first if File.exist?(changelog) end diff --git a/spec/bundler/support/path.rb b/spec/bundler/support/path.rb index 550a59408869a3..6b81cb19963dcf 100644 --- a/spec/bundler/support/path.rb +++ b/spec/bundler/support/path.rb @@ -291,7 +291,7 @@ def replace_required_ruby_version(version, dir:) end def replace_changelog(version, dir:) - changelog = File.expand_path("CHANGELOG.md", dir) + changelog = File.expand_path("CHANGELOG-bundler.md", dir) contents = File.readlines(changelog) contents = [contents[0], contents[1], "## #{version} (2100-01-01)\n", *contents[3..-1]].join File.open(changelog, "w") {|f| f << contents } @@ -345,7 +345,7 @@ def git_ls_files(glob) end def tracked_files_glob - ruby_core? ? "libexec/bundle* lib/bundler lib/bundler.rb spec/bundler man/bundle*" : "exe/bundle exe/bundler lib/bundler lib/bundler.rb bundler.gemspec bundler/CHANGELOG.md bundler/LICENSE.md bundler/README.md" + ruby_core? ? "libexec/bundle* lib/bundler lib/bundler.rb spec/bundler man/bundle*" : "exe/bundle exe/bundler lib/bundler lib/bundler.rb bundler.gemspec CHANGELOG-bundler.md LICENSE-bundler.md README-bundler.md" end def lib_tracked_files_glob From d6c484c25f5c9eda9f0556ef10fd97bd14bc3c73 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 22 Jun 2026 19:54:14 +0900 Subject: [PATCH 07/11] [ruby/rubygems] Update remaining references to the flat Bundler layout Catch the leftover bundler/lib and bundler/exe references after the move: the dev binstubs (bin/mdl, bin/rubocop, bin/test-unit), .gitattributes, .codespellrc, the release tool version file, the rubygems_ext/ci_detector cross-reference NOTE comments, and the developer documentation. https://github.com/ruby/rubygems/commit/821f51e297 --- lib/bundler/ci_detector.rb | 2 +- lib/rubygems/ci_detector.rb | 2 +- lib/rubygems/platform.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/bundler/ci_detector.rb b/lib/bundler/ci_detector.rb index e5fedbdea8d383..227e127c71c229 100644 --- a/lib/bundler/ci_detector.rb +++ b/lib/bundler/ci_detector.rb @@ -3,7 +3,7 @@ module Bundler module CIDetector # NOTE: Any changes made here will need to be made to both lib/rubygems/ci_detector.rb and - # bundler/lib/bundler/ci_detector.rb (which are enforced duplicates). + # lib/bundler/ci_detector.rb (which are enforced duplicates). # TODO: Drop that duplication once bundler drops support for RubyGems 3.4 # # ## Recognized CI providers, their signifiers, and the relevant docs ## diff --git a/lib/rubygems/ci_detector.rb b/lib/rubygems/ci_detector.rb index 7a2d4ee29a9360..22e9552079cae1 100644 --- a/lib/rubygems/ci_detector.rb +++ b/lib/rubygems/ci_detector.rb @@ -3,7 +3,7 @@ module Gem module CIDetector # NOTE: Any changes made here will need to be made to both lib/rubygems/ci_detector.rb and - # bundler/lib/bundler/ci_detector.rb (which are enforced duplicates). + # lib/bundler/ci_detector.rb (which are enforced duplicates). # TODO: Drop that duplication once bundler drops support for RubyGems 3.4 # # ## Recognized CI providers, their signifiers, and the relevant docs ## diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb index 367b00e7e1a286..dac1ce885829fe 100644 --- a/lib/rubygems/platform.rb +++ b/lib/rubygems/platform.rb @@ -196,7 +196,7 @@ def hash # :nodoc: # the runtime platform. # #-- - # NOTE: Until it can be removed, changes to this method must also be reflected in `bundler/lib/bundler/rubygems_ext.rb` + # NOTE: Until it can be removed, changes to this method must also be reflected in `lib/bundler/rubygems_ext.rb` def ===(other) return nil unless Gem::Platform === other @@ -221,7 +221,7 @@ def ===(other) end #-- - # NOTE: Until it can be removed, changes to this method must also be reflected in `bundler/lib/bundler/rubygems_ext.rb` + # NOTE: Until it can be removed, changes to this method must also be reflected in `lib/bundler/rubygems_ext.rb` def normalized_linux_version return nil unless @version From 4eb711b8e92bd9f56f6ec6cd99aa97e5d3bc1b64 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 23 Jun 2026 06:38:37 +0900 Subject: [PATCH 08/11] [ruby/rubygems] Adjust Bundler specs for the flat layout After flattening, Bundler shares lib/ with RubyGems, so spec subprocesses load the in-development Bundler from $LOAD_PATH instead of an installed gem, the same way ruby-core already does. Four examples assumed a separately installed Bundler and need updating. The load-order example pre-activates bundler unconditionally (was ruby_core? only) so Bundler.setup does not append the default bundler gem. "does not reveal system gems" relied on bundler showing up in installed_specs as an installed gem; flattened, bundler loads from $LOAD_PATH and no longer appears there. Assert the example's actual intent instead: the bundled gem stays visible and the system gem stays hidden across Gem.refresh. "bundle update --bundler" used `the_bundle.include_gems`, whose check runs `ruby -e "require 'bundler'; Bundler.setup"`; the in-development Bundler on $LOAD_PATH wins and its setup mismatches the locked version, so both the bundler and myrack checks fail. Verify the resolved versions through the binstub instead (`bundle --version` and `bundle list`), where the self-manager activates the locked Bundler before reading the bundle. "shows culprit file and line" double-loaded vendored Thor because the system binstub activated the installed Bundler while $LOAD_PATH carried the in-development one. Run it through the dev binstub so Bundler resolves from a single place. https://github.com/ruby/rubygems/commit/2b7e876494 --- spec/bundler/commands/update_spec.rb | 14 ++++++++++---- spec/bundler/install/gemfile_spec.rb | 2 +- spec/bundler/runtime/setup_spec.rb | 19 ++++++++++++------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/spec/bundler/commands/update_spec.rb b/spec/bundler/commands/update_spec.rb index f100bdfb9edb55..907f81d756ff7e 100644 --- a/spec/bundler/commands/update_spec.rb +++ b/spec/bundler/commands/update_spec.rb @@ -1582,8 +1582,11 @@ 999.0.0 L - expect(the_bundle).to include_gems "bundler 999.0.0" - expect(the_bundle).to include_gems "myrack 1.0" + bundle "--version" + expect(out).to include("999.0.0") + + bundle "list" + expect(out).to include("myrack (1.0)") end it "does not claim to update to Bundler version to a wrong version when cached gems are present" do @@ -1665,8 +1668,11 @@ 9.9.9 L - expect(the_bundle).to include_gems "bundler 9.9.9" - expect(the_bundle).to include_gems "myrack 1.0" + bundle "--version" + expect(out).to include("9.9.9") + + bundle "list" + expect(out).to include("myrack (1.0)") end it "errors if the explicit target version does not exist" do diff --git a/spec/bundler/install/gemfile_spec.rb b/spec/bundler/install/gemfile_spec.rb index 83875a3d0e300f..b8cd66ca7580ce 100644 --- a/spec/bundler/install/gemfile_spec.rb +++ b/spec/bundler/install/gemfile_spec.rb @@ -135,7 +135,7 @@ def source(source, *args, &blk) it "shows culprit file and line" do skip "ruby-core test setup has always \"lib\" in $LOAD_PATH so `require \"bundler\"` always activates the local version rather than using RubyGems gem activation stuff, causing conflicts" if ruby_core? - install_gemfile "source 'https://gem.repo1'", requires: [bundler_bug], artifice: nil, raise_on_error: false + install_gemfile "source 'https://gem.repo1'", requires: [bundler_bug], artifice: nil, raise_on_error: false, bundle_bin: dev_binstub expect(err).to include("bundler_bug.rb:6") end end diff --git a/spec/bundler/runtime/setup_spec.rb b/spec/bundler/runtime/setup_spec.rb index ceb6fcf66acf39..2b4ac3085c318b 100644 --- a/spec/bundler/runtime/setup_spec.rb +++ b/spec/bundler/runtime/setup_spec.rb @@ -144,7 +144,7 @@ def clean_load_path(lp) ruby <<-RUBY require 'bundler' - gem "bundler", "#{Bundler::VERSION}" if #{ruby_core?} + gem "bundler", "#{Bundler::VERSION}" Bundler.setup puts $LOAD_PATH RUBY @@ -987,7 +987,8 @@ def clean_load_path(lp) puts Bundler.rubygems.installed_specs.map(&:name) R - expect(out).to eq("activesupport\nbundler\nactivesupport\nbundler") + expect(out).to include("activesupport") + expect(out).not_to include("myrack") end describe "when a vendored gem specification uses the :path option" do @@ -1309,12 +1310,16 @@ def lock_with(ruby_version = nil) end context "is not present" do - # Skipped on ruby-core because `ruby "require 'bundler/setup'"` does not - # activate bundler as a gem there, so Source::Metadata falls back to a - # synthetic spec whose cache_file does not exist on disk and - # LockfileGenerator#bundler_checksum drops the bundler checksum, while - # the on-disk lockfile still has it. + # Skipped on ruby-core, and on the release-version CI variant, because + # `ruby "require 'bundler/setup'"` does not activate bundler as a gem + # there, so Source::Metadata falls back to a synthetic spec whose + # cache_file does not exist on disk and LockfileGenerator#bundler_checksum + # drops the bundler checksum, while the on-disk lockfile still has it. + # In-development (.dev) versions never write a bundler checksum, so the + # regular suite stays unaffected. it "does not change the lock", :ruby_repo do + skip "bundler is loaded from the source tree, not installed as a gem" unless Bundler::VERSION.end_with?(".dev") + expect { ruby "require 'bundler/setup'" }.not_to change { lockfile } end end From 8ef77c3dfe1ec31fd9a7e1cdc0e371978fb17427 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 23 Jun 2026 14:27:39 +0900 Subject: [PATCH 09/11] [ruby/rubygems] Load the worktree RubyGems in the dev binstubs After flattening, lib/ holds both RubyGems and Bundler, so any dev binstub that puts the worktree lib/ on $LOAD_PATH now overlays the already-booted system RubyGems with the worktree one. bin/bundle hit this through the Bundler gemspec activation: the partial overlay mixed a worktree Gem with an older system Gem::ConfigFile, breaking native extension installs on Ruby 3.2/3.3 (undefined install_extension_in_lib) and corrupting spec names elsewhere. The same overlay breaks bin/rake, bin/ronn, bin/rubocop and bin/mdl, which unshift the worktree lib/ (or require it through rubygems_ext) but boot on the system RubyGems. On a host whose RubyGems differs from the worktree copy this double-loads files like rubygems/package and raises (undefined method `spec=' for class Gem::Package::Old). Route all of them through switch_rubygems, like bin/rspec and bin/test-unit already do, so they re-exec onto the worktree RubyGems before anything is loaded. Point the shared gem home job at the worktree copy too, since the dev binstub can no longer load a separately installed RubyGems, and the matrix is redundant for now. https://github.com/ruby/rubygems/commit/f50eb4216d --- spec/bin/bundle | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/bin/bundle b/spec/bin/bundle index 8f8b5352951537..23e386505d6e7a 100755 --- a/spec/bin/bundle +++ b/spec/bin/bundle @@ -1,6 +1,7 @@ #!/usr/bin/env ruby # frozen_string_literal: true +require_relative "../bundler/support/switch_rubygems" require_relative "../bundler/support/activate" load File.expand_path("bundle", Spec::Path.exedir) From 6018a38aa68a88082b581bd30b037c1a8f9779f6 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 24 Jun 2026 13:15:47 +0900 Subject: [PATCH 10/11] Follow the flattened Bundler layout when syncing rubygems ruby/rubygems flattened the Bundler tree from bundler/ up to the repository root, so repoint the rubygems mappings accordingly. Guard the gemspec fixup with File.exist? too, since the synthetic parent tree built for the flattening commit has no lib/bundler/bundler.gemspec yet. https://github.com/ruby/rubygems/pull/9634 Co-Authored-By: Claude Opus 4.8 --- tool/sync_default_gems.rb | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/tool/sync_default_gems.rb b/tool/sync_default_gems.rb index db64e20274b4e0..3242dee5d2b541 100755 --- a/tool/sync_default_gems.rb +++ b/tool/sync_default_gems.rb @@ -241,11 +241,11 @@ def lib((upstream, branch), gemspec_in_subdir: false) ["lib/rubygems.rb", "lib/rubygems.rb"], ["lib/rubygems", "lib/rubygems"], ["test/rubygems", "test/rubygems"], - ["bundler/lib/bundler.rb", "lib/bundler.rb"], - ["bundler/lib/bundler", "lib/bundler"], - ["bundler/exe/bundle", "libexec/bundle"], - ["bundler/exe/bundler", "libexec/bundler"], - ["bundler/bundler.gemspec", "lib/bundler/bundler.gemspec"], + ["lib/bundler.rb", "lib/bundler.rb"], + ["lib/bundler", "lib/bundler"], + ["exe/bundle", "libexec/bundle"], + ["exe/bundler", "libexec/bundler"], + ["bundler.gemspec", "lib/bundler/bundler.gemspec"], ["spec", "spec/bundler"], *["bundle", "parallel_rspec", "rspec"].map {|binstub| ["bin/#{binstub}", "spec/bin/#{binstub}"] @@ -369,12 +369,15 @@ def replace_rdoc_ref_all_full end def rubygems_do_fixup - gemspec_content = File.readlines("lib/bundler/bundler.gemspec").map do |line| - next if line =~ /LICENSE\.md/ + gemspec = "lib/bundler/bundler.gemspec" + if File.exist?(gemspec) + gemspec_content = File.readlines(gemspec).map do |line| + next if line =~ /LICENSE\.md/ - line.gsub("bundler.gemspec", "lib/bundler/bundler.gemspec") - end.compact.join - File.write("lib/bundler/bundler.gemspec", gemspec_content) + line.gsub("bundler.gemspec", "lib/bundler/bundler.gemspec") + end.compact.join + File.write(gemspec, gemspec_content) + end ["bundle", "parallel_rspec", "rspec"].each do |binstub| path = "spec/bin/#{binstub}" From 815710fc27bd3c12d256e14883b37d2a3c906802 Mon Sep 17 00:00:00 2001 From: Jeremy Evans Date: Tue, 23 Jun 2026 20:19:17 -0700 Subject: [PATCH 11/11] [DOC] Update documentation of refinement super Method lookup for refinement super starts with the next active refinement at the super call site, excluding the current refinment, if such a refinement exists. This has been the behavior since Ruby 2.7, and it is now confirmed to be the desired behavior. --- doc/syntax/refinements.rdoc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/syntax/refinements.rdoc b/doc/syntax/refinements.rdoc index 80595eb4455ab6..836dac2c99ad15 100644 --- a/doc/syntax/refinements.rdoc +++ b/doc/syntax/refinements.rdoc @@ -246,7 +246,10 @@ invokes that method since +foo+ does not exist on Integer. When +super+ is invoked, method lookup starts: -* If the method is in a refinement, at the refined class or module +* If the method is in a refinement: + * At the next active refinement in scope at the +super+ call site, + excluding the current refinement, if such a refinement exists + * Otherwise, at the refined class or module * Otherwise, at the next ancestor Method lookup then proceeds as described in the Method Lookup section