From 72b2e47376ba30103e9eeec3c732dcab4debb4eb Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 14 Sep 2026 18:45:09 +0900 Subject: [PATCH 01/10] Keep the Ruby::Box experimental warning out of test subprocess output Under RUBY_BOX=1 every spawned ruby prints an experimental warning to stderr, which breaks assertions on subprocess output in both suites. The Bundler specs filter the captured stderr instead of adding -W:no-experimental to RUBYOPT, because several of them assert the exact RUBYOPT propagated to subprocesses. Co-Authored-By: Claude Opus 5 --- spec/support/command_execution.rb | 15 ++++++++++++++- test/rubygems/helper.rb | 11 +++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/spec/support/command_execution.rb b/spec/support/command_execution.rb index e2915b996d9d..14da54dca4da 100644 --- a/spec/support/command_execution.rb +++ b/spec/support/command_execution.rb @@ -2,6 +2,13 @@ module Spec class CommandExecution + # Under RUBY_BOX, every spawned ruby prints an experimental warning to + # stderr, breaking specs that assert clean stderr. + RUBY_BOX_WARNING = Regexp.union( + /^[^\n]*: warning: Ruby::Box is experimental, and the behavior may change in the future!\n?/, + %r{^See https://docs\.ruby-lang\.org/\S+ for known issues, etc\.\n?} + ) + def initialize(command, timeout:) @command = command @timeout = timeout @@ -72,7 +79,13 @@ def failure? attr_reader :failure_reason def normalize(string) - string.dup.force_encoding(Encoding::UTF_8).scrub.strip.gsub("\r\n", "\n") + string = string.dup.force_encoding(Encoding::UTF_8).scrub.gsub("\r\n", "\n") + string = string.gsub(RUBY_BOX_WARNING, "") if ruby_box_enabled? + string.strip + end + + def ruby_box_enabled? + defined?(Ruby::Box) && Ruby::Box.enabled? end end end diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index 09b7f427ebbf..f2203675c957 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -404,6 +404,10 @@ def setup ENV["BUNDLE_COOLDOWN"] = nil ENV["RUBYGEMS_PREVENT_UPDATE_SUGGESTION"] = "true" + # Child ruby processes inherit RUBY_BOX and print an experimental + # warning on startup, breaking assertions on subprocess stderr. + ENV["RUBYOPT"] = [ENV["RUBYOPT"], "-W:no-experimental"].compact.join(" ") if ruby_box_enabled? + @current_dir = Dir.pwd @fetcher = nil @@ -1440,6 +1444,13 @@ def ruby_repo? !ENV["GEM_COMMAND"].nil? end + ## + # Is this test running under Ruby::Box (RUBY_BOX=1)? + + def ruby_box_enabled? + defined?(Ruby::Box) && Ruby::Box.enabled? + end + ## # Returns the make command for the current platform. For versions of Ruby # built on MS Windows with VC++ or Borland it will return 'nmake'. On all From 67147d48a1e9c3b62a385cc8950c4a6a3b92f1bc Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 14 Sep 2026 18:45:31 +0900 Subject: [PATCH 02/10] Use Process.last_status instead of $? Ruby::Box leaves $? uninitialized, so status checks read success after a failing command. Under RUBY_BOX=1 this made Gem::Source::Git#rev_parse swallow rev-parse failures and the release tool carry on with empty output. https://bugs.ruby-lang.org/issues/22280 Co-Authored-By: Claude Opus 5 --- lib/bundler/gem_helper.rb | 2 +- lib/rubygems/source/git.rb | 4 +++- spec/runtime/env_helpers_spec.rb | 12 ++++++------ test/rubygems/test_exit.rb | 3 ++- test/rubygems/test_gem_ext_builder.rb | 2 +- test/rubygems/test_gem_ext_cargo_builder.rb | 2 +- test/rubygems/test_require.rb | 4 ++-- test/rubygems/test_rubygems.rb | 4 ++-- tool/release.rb | 8 ++++---- 9 files changed, 22 insertions(+), 19 deletions(-) diff --git a/lib/bundler/gem_helper.rb b/lib/bundler/gem_helper.rb index e3af1b957f35..ea02e7e7712b 100644 --- a/lib/bundler/gem_helper.rb +++ b/lib/bundler/gem_helper.rb @@ -216,7 +216,7 @@ def sh_with_status(cmd, &block) Bundler.ui.debug(cmd) SharedHelpers.chdir(base) do outbuf = IO.popen(cmd, err: [:child, :out], &:read) - status = $? + status = Process.last_status block&.call(outbuf) if status.success? [outbuf, status] end diff --git a/lib/rubygems/source/git.rb b/lib/rubygems/source/git.rb index baf2f9dd4c09..0ceb67b382f5 100644 --- a/lib/rubygems/source/git.rb +++ b/lib/rubygems/source/git.rb @@ -188,9 +188,11 @@ def rev_parse # :nodoc: hash = Gem::Util.popen(git_command, "rev-parse", @reference).strip end + # Process.last_status instead of $?, which Ruby::Box leaves uninitialized + # (https://bugs.ruby-lang.org/issues/22280) raise Gem::Exception, "unable to find reference #{@reference} in #{@repository}" unless - $?.success? + Process.last_status.success? hash end diff --git a/spec/runtime/env_helpers_spec.rb b/spec/runtime/env_helpers_spec.rb index 38f501e7cde6..0ebf86f29606 100644 --- a/spec/runtime/env_helpers_spec.rb +++ b/spec/runtime/env_helpers_spec.rb @@ -167,13 +167,13 @@ def run_bundler_script(env, script) create_file("source.rb", <<-'RUBY') Bundler.original_system("ruby", "-e", "exit(42) if ENV['BUNDLE_FOO'] == 'bar'") - exit $?.exitstatus + exit Process.last_status.exitstatus RUBY end it "runs system inside with_original_env" do run_bundler_script({ "BUNDLE_FOO" => "bar" }, bundled_app("source.rb")) - expect($?.exitstatus).to eq(42) + expect(Process.last_status.exitstatus).to eq(42) end end @@ -182,13 +182,13 @@ def run_bundler_script(env, script) create_file("source.rb", <<-'RUBY') Bundler.unbundled_system("ruby", "-e", "exit(42) unless ENV['BUNDLE_FOO'] == 'bar'") - exit $?.exitstatus + exit Process.last_status.exitstatus RUBY end it "runs system inside with_unbundled_env" do run_bundler_script({ "BUNDLE_FOO" => "bar" }, bundled_app("source.rb")) - expect($?.exitstatus).to eq(42) + expect(Process.last_status.exitstatus).to eq(42) end end @@ -209,7 +209,7 @@ def run_bundler_script(env, script) skip "Fork not implemented" if Gem.win_platform? run_bundler_script({ "BUNDLE_FOO" => "bar" }, bundled_app("source.rb")) - expect($?.exitstatus).to eq(0) + expect(Process.last_status.exitstatus).to eq(0) end end @@ -230,7 +230,7 @@ def run_bundler_script(env, script) skip "Fork not implemented" if Gem.win_platform? run_bundler_script({ "BUNDLE_FOO" => "bar" }, bundled_app("source.rb")) - expect($?.exitstatus).to eq(1) + expect(Process.last_status.exitstatus).to eq(1) end end end diff --git a/test/rubygems/test_exit.rb b/test/rubygems/test_exit.rb index 396837edadfa..c339c39a67af 100644 --- a/test/rubygems/test_exit.rb +++ b/test/rubygems/test_exit.rb @@ -6,7 +6,8 @@ class TestGemExit < Gem::TestCase def test_exit system(*ruby_with_rubygems_in_load_path, "-e", "raise Gem::SystemExitException.new(2)") - assert_equal 2, $?.exitstatus + # Process.last_status instead of $?, which Ruby::Box leaves uninitialized + assert_equal 2, Process.last_status.exitstatus end def test_status diff --git a/test/rubygems/test_gem_ext_builder.rb b/test/rubygems/test_gem_ext_builder.rb index 6b4eed2cf211..f5ad791ec1c9 100644 --- a/test/rubygems/test_gem_ext_builder.rb +++ b/test/rubygems/test_gem_ext_builder.rb @@ -700,7 +700,7 @@ def self.expand(val, config = CONFIG); val; end system(Gem.ruby, "-rmkmf", "-e", "exit MakeMakefile::RbConfig::CONFIG['host_os'] == 'fake_os'", "--", "--target-rbconfig=#{fake_rbconfig}") end - unless $?.success? + unless Process.last_status.success? assert_include(stderr, "uninitialized constant MakeMakefile::RbConfig") pend "This version of mkmf does not support --target-rbconfig" end diff --git a/test/rubygems/test_gem_ext_cargo_builder.rb b/test/rubygems/test_gem_ext_cargo_builder.rb index b970e442c250..94673a478c4e 100644 --- a/test/rubygems/test_gem_ext_cargo_builder.rb +++ b/test/rubygems/test_gem_ext_cargo_builder.rb @@ -199,7 +199,7 @@ def skip_unsupported_platforms! pend "jruby not supported" if Gem.java_platform? pend "truffleruby not supported (yet)" if RUBY_ENGINE == "truffleruby" system(@rust_envs, "cargo", "-V", out: IO::NULL, err: [:child, :out]) - pend "cargo not present" unless $?.success? + pend "cargo not present" unless Process.last_status.success? pend "ruby.h is not provided by ruby repo" if ruby_repo? pend "rust toolchain of mingw is broken" if mingw_windows? end diff --git a/test/rubygems/test_require.rb b/test/rubygems/test_require.rb index db86a3090565..6816e42bfe13 100644 --- a/test/rubygems/test_require.rb +++ b/test/rubygems/test_require.rb @@ -484,7 +484,7 @@ def test_realworld_default_gem puts Gem.loaded_specs["json"] RUBY output = Gem::Util.popen(*ruby_with_rubygems_in_load_path, "-e", cmd).strip - assert $?.success? + assert Process.last_status.success? refute_empty output end @@ -508,7 +508,7 @@ def test_realworld_upgraded_default_gem assert_equal "999.99.9", output.lines[0].chomp # Make sure only files from the newer json gem are loaded, and no files from the default json gem assert_equal ["#{@gemhome}/gems/json-999.99.9/lib/json.rb"], output.lines.grep(%r{/gems/json-}).map(&:chomp) - assert $?.success? + assert Process.last_status.success? end def test_default_gem_and_normal_gem diff --git a/test/rubygems/test_rubygems.rb b/test/rubygems/test_rubygems.rb index 6566b5981e69..02393c57fcae 100644 --- a/test/rubygems/test_rubygems.rb +++ b/test/rubygems/test_rubygems.rb @@ -5,7 +5,7 @@ class GemTest < Gem::TestCase def test_rubygems_normal_behaviour _ = Gem::Util.popen(*ruby_with_rubygems_in_load_path, "-e", "'require \"rubygems\"'", { err: [:child, :out] }).strip - assert $?.success? + assert Process.last_status.success? end def test_operating_system_other_exceptions @@ -17,7 +17,7 @@ def test_operating_system_other_exceptions RUBY output = Gem::Util.popen(*ruby_with_rubygems_and_fake_operating_system_in_load_path(path), "-e", "'require \"rubygems\"'", { err: [:child, :out] }).strip - assert !$?.success? + assert !Process.last_status.success? assert_match(/undefined local variable or method [`']intentionally_not_implemented_method'/, output) assert_includes output, "Loading the #{operating_system_rb_at(path)} file caused an error. " \ "This file is owned by your OS, not by rubygems upstream. " \ diff --git a/tool/release.rb b/tool/release.rb index 7585136b6c4f..2324b2d0f95f 100644 --- a/tool/release.rb +++ b/tool/release.rb @@ -442,7 +442,7 @@ def add_commit_authors!(pulls) ids = batch.flat_map {|pull| ["-F", "ids[]=#{pull.node_id}"] } json = IO.popen(["gh", "api", "graphql", "-f", "query=#{COMMIT_AUTHORS_QUERY}", *ids], &:read) - raise "Failed to list the commits of #{batch.map(&:number).join(", ")}" unless $?.success? + raise "Failed to list the commits of #{batch.map(&:number).join(", ")}" unless Process.last_status.success? credit_commit_authors(batch, JSON.parse(json).dig("data", "nodes")) end @@ -501,7 +501,7 @@ def git_quietly(*args) def pull_requests_merged_into(base, from, to) commits = git_quietly("rev-list", "#{from}..#{to}") - raise "Failed to list the commits in #{from}..#{to}" unless $?.success? + raise "Failed to list the commits in #{from}..#{to}" unless Process.last_status.success? reachable = Set.new(commits.split("\n")) @@ -511,12 +511,12 @@ def pull_requests_merged_into(base, from, to) # The date bound is deliberately loose. It bounds the query, not the result. def merged_pull_requests(base, since_ref) committed_at = git_quietly("log", "-1", "--format=%cI", since_ref).strip - raise "Failed to resolve #{since_ref}" unless $?.success? + raise "Failed to resolve #{since_ref}" unless Process.last_status.success? since = (Time.iso8601(committed_at) - 86_400).utc.strftime("%Y-%m-%d") json = `gh pr list --repo ruby/rubygems --state merged --base #{base} --search 'merged:>=#{since}' --limit #{MERGED_PULL_REQUEST_LIMIT} --json number,id,title,labels,mergeCommit,mergedAt,author,url` - raise "Failed to list pull requests merged into #{base} since #{since}" unless $?.success? + raise "Failed to list pull requests merged into #{base} since #{since}" unless Process.last_status.success? pull_requests_from(json, "#{base} since #{since}") end From ec8b8db347cf6b8af0bc5e249c013ad0ac675b0d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 14 Sep 2026 18:45:59 +0900 Subject: [PATCH 03/10] Pend tests that capture stdio under Ruby::Box Each box gets detached copies of the stdio globals, so reassigning $stdout or $stderr cannot capture output written by Kernel#warn, Kernel#puts or subprocesses. https://bugs.ruby-lang.org/issues/21867 Co-Authored-By: Claude Opus 5 --- spec/bundler/plugin_spec.rb | 6 ++++++ test/rubygems/helper.rb | 10 ++++++++++ test/rubygems/test_deprecate.rb | 3 +++ test/rubygems/test_gem.rb | 2 ++ test/rubygems/test_gem_commands_build_command.rb | 1 + test/rubygems/test_gem_commands_open_command.rb | 1 + test/rubygems/test_gem_config_file.rb | 1 + test/rubygems/test_gem_doctor.rb | 2 ++ test/rubygems/test_gem_package.rb | 2 ++ test/rubygems/test_gem_request_set.rb | 2 ++ .../test_gem_request_set_gem_dependency_api.rb | 3 +++ test/rubygems/test_gem_specification.rb | 5 +++++ test/rubygems/test_gem_stub_specification.rb | 1 + test/rubygems/test_require.rb | 4 ++++ 14 files changed, 43 insertions(+) diff --git a/spec/bundler/plugin_spec.rb b/spec/bundler/plugin_spec.rb index 1e10036b63c2..5f5054b68425 100644 --- a/spec/bundler/plugin_spec.rb +++ b/spec/bundler/plugin_spec.rb @@ -318,6 +318,8 @@ end it "executes the hook" do + skip "Ruby::Box ignores $stdout reassignment (https://bugs.ruby-lang.org/issues/21867)" if defined?(Ruby::Box) && Ruby::Box.enabled? + expect do Plugin.hook(Bundler::Plugin::Events::EVENT1) end.to output("hook for event 1\n").to_stdout @@ -331,6 +333,8 @@ RUBY it "evals plugins.rb once" do + skip "Ruby::Box ignores $stdout reassignment (https://bugs.ruby-lang.org/issues/21867)" if defined?(Ruby::Box) && Ruby::Box.enabled? + expect do Plugin.hook(Bundler::Plugin::Events::EVENT1) Plugin.hook(Bundler::Plugin::Events::EVENT2) @@ -344,6 +348,8 @@ RUBY it "is passed to the hook" do + skip "Ruby::Box ignores $stdout reassignment (https://bugs.ruby-lang.org/issues/21867)" if defined?(Ruby::Box) && Ruby::Box.enabled? + expect do Plugin.hook(Bundler::Plugin::Events::EVENT1) { puts "win" } end.to output("win\n").to_stdout diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index f2203675c957..38ac67be3add 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -1451,6 +1451,16 @@ def ruby_box_enabled? defined?(Ruby::Box) && Ruby::Box.enabled? end + ## + # Ruby::Box gives each box detached copies of the stdio globals, so + # reassigning $stdout/$stderr cannot capture output written by Kernel#warn, + # Kernel#puts or subprocesses. Pends until the ruby-core fix for + # https://bugs.ruby-lang.org/issues/21867 lands. + + def pend_for_ruby_box_stdio_capture + pend "Ruby::Box breaks $stdout/$stderr capture (https://bugs.ruby-lang.org/issues/21867)" if ruby_box_enabled? + end + ## # Returns the make command for the current platform. For versions of Ruby # built on MS Windows with VC++ or Borland it will return 'nmake'. On all diff --git a/test/rubygems/test_deprecate.rb b/test/rubygems/test_deprecate.rb index bb6a0b5ceaaf..5700d356e756 100644 --- a/test/rubygems/test_deprecate.rb +++ b/test/rubygems/test_deprecate.rb @@ -132,6 +132,7 @@ def test_deprecated_method_calls_the_old_method end def test_deprecated_method_outputs_a_warning + pend_for_ruby_box_stdio_capture out, err = capture_output do thing = Thing.new thing.foo @@ -165,6 +166,7 @@ def execute end def test_deprecated_method_outputs_a_warning_old_way + pend_for_ruby_box_stdio_capture out, err = capture_output do thing = OtherThing.new thing.foo @@ -180,6 +182,7 @@ def test_deprecated_method_outputs_a_warning_old_way end def test_deprecated_method_when_class_overrides_format + pend_for_ruby_box_stdio_capture out, err = capture_output do thing = ThingWithFormat.new thing.foo diff --git a/test/rubygems/test_gem.rb b/test/rubygems/test_gem.rb index 88b461a2a0a7..36067549c863 100644 --- a/test/rubygems/test_gem.rb +++ b/test/rubygems/test_gem.rb @@ -1297,6 +1297,7 @@ def test_self_try_activate_missing_prerelease end def test_self_try_activate_missing_extensions + pend_for_ruby_box_stdio_capture spec = util_spec "ext", "1" do |s| s.extensions = %w[ext/extconf.rb] s.installed_by_version = v("2.2") @@ -1352,6 +1353,7 @@ def test_setting_paths_does_not_mutate_parameter_object end def test_deprecated_paths= + pend_for_ruby_box_stdio_capture stdout, stderr = capture_output do Gem.paths = { "GEM_HOME" => Gem.paths.home, "GEM_PATH" => [Gem.paths.home, "foo"] } diff --git a/test/rubygems/test_gem_commands_build_command.rb b/test/rubygems/test_gem_commands_build_command.rb index 771eb07dbc9c..5bbb7c3b3e35 100644 --- a/test/rubygems/test_gem_commands_build_command.rb +++ b/test/rubygems/test_gem_commands_build_command.rb @@ -383,6 +383,7 @@ def test_execute_strict_with_warnings end def test_execute_bad_spec + pend_for_ruby_box_stdio_capture @gem.date = "2010-11-08" gemspec_file = File.join(@tempdir, @gem.spec_name) diff --git a/test/rubygems/test_gem_commands_open_command.rb b/test/rubygems/test_gem_commands_open_command.rb index 3a774a9343c0..c30117a58ecc 100644 --- a/test/rubygems/test_gem_commands_open_command.rb +++ b/test/rubygems/test_gem_commands_open_command.rb @@ -21,6 +21,7 @@ def gem(name, version = "1.0") end def test_execute + pend_for_ruby_box_stdio_capture omit "JRuby on Windows spawns the editor with a different cwd" if Gem.win_platform? && Gem.java_platform? @cmd.options[:args] = %w[foo] diff --git a/test/rubygems/test_gem_config_file.rb b/test/rubygems/test_gem_config_file.rb index 2c33192a4b3a..7120c49e327a 100644 --- a/test/rubygems/test_gem_config_file.rb +++ b/test/rubygems/test_gem_config_file.rb @@ -314,6 +314,7 @@ def test_handle_arguments_backtrace end def test_handle_arguments_debug + pend_for_ruby_box_stdio_capture assert_equal false, $DEBUG args = %w[--debug] diff --git a/test/rubygems/test_gem_doctor.rb b/test/rubygems/test_gem_doctor.rb index 1554e7af128d..da625000ff90 100644 --- a/test/rubygems/test_gem_doctor.rb +++ b/test/rubygems/test_gem_doctor.rb @@ -240,6 +240,7 @@ def test_doctor_preserves_valid_abi_scoped_gemspec end def test_doctor_removes_corrupt_abi_scoped_gemspec + pend_for_ruby_box_stdio_capture install_specs util_spec "regular_gem" spec = util_ca_spec "ca_gem", "1", "aabbccdd", @@ -286,6 +287,7 @@ def test_doctor_preserves_other_abi_dir end def test_doctor_does_not_recurse_into_abi_symlink + pend_for_ruby_box_stdio_capture pend "symlinks not supported" unless symlink_supported? install_specs util_spec "regular_gem" diff --git a/test/rubygems/test_gem_package.rb b/test/rubygems/test_gem_package.rb index b0935693d1c7..4e83b3a69b46 100644 --- a/test/rubygems/test_gem_package.rb +++ b/test/rubygems/test_gem_package.rb @@ -1538,6 +1538,7 @@ def test_verify_corrupt end def test_verify_corrupt_tar_metadata_entry + pend_for_ruby_box_stdio_capture gem = tar_file_header("metadata.gz", "", 0, 999, Time.now) File.open "corrupt.gem", "wb" do |io| @@ -1574,6 +1575,7 @@ def test_verify_corrupt_tar_checksums_entry end def test_verify_corrupt_tar_data_entry + pend_for_ruby_box_stdio_capture gem = tar_file_header("data.tar.gz", "", 0, 100, Time.now) File.open "corrupt.gem", "wb" do |io| diff --git a/test/rubygems/test_gem_request_set.rb b/test/rubygems/test_gem_request_set.rb index 8c8be04fb9f9..60ff8724aef4 100644 --- a/test/rubygems/test_gem_request_set.rb +++ b/test/rubygems/test_gem_request_set.rb @@ -71,6 +71,7 @@ def test_install_from_gemdeps end def test_install_from_gemdeps_explain + pend_for_ruby_box_stdio_capture spec_fetcher do |fetcher| fetcher.gem "a", 2 end @@ -94,6 +95,7 @@ def test_install_from_gemdeps_explain end def test_install_from_gemdeps_explain_verbose + pend_for_ruby_box_stdio_capture spec_fetcher do |fetcher| fetcher.gem "a", 2 end diff --git a/test/rubygems/test_gem_request_set_gem_dependency_api.rb b/test/rubygems/test_gem_request_set_gem_dependency_api.rb index 4b5eaa38eda8..d8f4e7f6e92b 100644 --- a/test/rubygems/test_gem_request_set_gem_dependency_api.rb +++ b/test/rubygems/test_gem_request_set_gem_dependency_api.rb @@ -78,6 +78,7 @@ def test_gem end def test_gem_duplicate + pend_for_ruby_box_stdio_capture @gda.gem "a" _, err = capture_output do @@ -128,6 +129,7 @@ def test_gem_bitbucket_expand_path end def test_gem_git_branch + pend_for_ruby_box_stdio_capture _, err = capture_output do @gda.gem "a", git: "git/a", branch: "other", tag: "v1" end @@ -149,6 +151,7 @@ def test_gem_git_gist end def test_gem_git_ref + pend_for_ruby_box_stdio_capture _, err = capture_output do @gda.gem "a", git: "git/a", ref: "abcd123", branch: "other" end diff --git a/test/rubygems/test_gem_specification.rb b/test/rubygems/test_gem_specification.rb index dc32a6290786..0f107d12529a 100644 --- a/test/rubygems/test_gem_specification.rb +++ b/test/rubygems/test_gem_specification.rb @@ -1576,6 +1576,7 @@ def test_contains_requirable_file_eh end def test_contains_requirable_file_eh_extension + pend_for_ruby_box_stdio_capture ext_spec _, err = capture_output do @@ -3386,6 +3387,7 @@ def test_validate_files end def test_unresolved_specs + pend_for_ruby_box_stdio_capture specification = Gem::Specification.clone set_orig specification @@ -3412,6 +3414,7 @@ def test_unresolved_specs end def test_unresolved_specs_with_versions + pend_for_ruby_box_stdio_capture specification = Gem::Specification.clone set_orig specification @@ -3444,6 +3447,7 @@ def test_unresolved_specs_with_versions end def test_unresolved_specs_with_duplicated_versions + pend_for_ruby_box_stdio_capture specification = Gem::Specification.clone set_orig specification @@ -3497,6 +3501,7 @@ def test_unresolved_specs_with_unrestricted_deps_on_default_gems end def test_duplicate_runtime_dependency + pend_for_ruby_box_stdio_capture expected = "WARNING: duplicated b dependency [\"~> 3.0\", \"~> 3.0\"]\n" out, err = capture_output do @a1.add_dependency "b", "~> 3.0", "~> 3.0" diff --git a/test/rubygems/test_gem_stub_specification.rb b/test/rubygems/test_gem_stub_specification.rb index 1aa3b6532436..66bdd3d3fbbb 100644 --- a/test/rubygems/test_gem_stub_specification.rb +++ b/test/rubygems/test_gem_stub_specification.rb @@ -94,6 +94,7 @@ def test_contains_requirable_file_eh end def test_contains_requirable_file_eh_extension + pend_for_ruby_box_stdio_capture stub_with_extension do |stub| _, err = capture_output do if RUBY_ENGINE == "jruby" diff --git a/test/rubygems/test_require.rb b/test/rubygems/test_require.rb index 6816e42bfe13..ef1bb2e465a2 100644 --- a/test/rubygems/test_require.rb +++ b/test/rubygems/test_require.rb @@ -718,6 +718,7 @@ def test_require_bundler ["", "Kernel."].each do |prefix| define_method "test_no_kernel_require_in_#{prefix.tr(".", "_")}warn_with_uplevel" do + pend_for_ruby_box_stdio_capture Dir.mktmpdir("warn_test") do |dir| File.write(dir + "/sub.rb", "#{prefix}warn 'uplevel', 'test', uplevel: 1\n") File.write(dir + "/main.rb", "require 'sub'\n") @@ -733,6 +734,7 @@ def test_require_bundler end define_method "test_no_other_behavioral_changes_with_#{prefix.tr(".", "_")}warn" do + pend_for_ruby_box_stdio_capture Dir.mktmpdir("warn_test") do |dir| File.write(dir + "/main.rb", "#{prefix}warn({x:1}, {y:2}, [])\n") _, err = capture_subprocess_io do @@ -748,6 +750,7 @@ def test_require_bundler end def test_no_crash_when_overriding_warn_with_warning_module + pend_for_ruby_box_stdio_capture Dir.mktmpdir("warn_test") do |dir| File.write(dir + "/main.rb", "module Warning; def warn(str); super; end; end; warn 'Foo Bar'") _, err = capture_subprocess_io do @@ -762,6 +765,7 @@ def test_no_crash_when_overriding_warn_with_warning_module end def test_expected_backtrace_location_when_inheriting_from_basic_object_and_including_kernel + pend_for_ruby_box_stdio_capture Dir.mktmpdir("warn_test") do |dir| File.write(dir + "/main.rb", "\nrequire 'sub'\n") File.write(dir + "/sub.rb", <<-'RUBY') From 90bdecc0dcd745b595be4c4a57c3f1ae72d2d771 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 14 Sep 2026 18:46:09 +0900 Subject: [PATCH 04/10] Pend the Ractor tar header tests under Ruby::Box Ruby::Box ignores $VERBOSE=, so assert_ractor cannot keep the Ractor experimental warning out of the child stderr. https://bugs.ruby-lang.org/issues/22282 Co-Authored-By: Claude Opus 5 --- test/rubygems/test_gem_package_tar_header_ractor.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/rubygems/test_gem_package_tar_header_ractor.rb b/test/rubygems/test_gem_package_tar_header_ractor.rb index 57140648052e..d2fc0f69b1e9 100644 --- a/test/rubygems/test_gem_package_tar_header_ractor.rb +++ b/test/rubygems/test_gem_package_tar_header_ractor.rb @@ -8,6 +8,11 @@ end class TestGemPackageTarHeaderRactor < Gem::Package::TarTestCase + def setup + super + pend "Ruby::Box ignores $VERBOSE=, so assert_ractor cannot keep the Ractor experimental warning out of the child stderr (https://bugs.ruby-lang.org/issues/22282)" if ruby_box_enabled? + end + SETUP = <<~RUBY header = { name: "x", From c94b20043319de83cb1b4c010fc4549319f9efa2 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 14 Sep 2026 18:46:37 +0900 Subject: [PATCH 05/10] Run the suites on a ruby-core master build with and without RUBY_BOX=1 The setup-ruby head build lags ruby/ruby by up to a day, so Ruby::Box fixes that have already landed would stay out of a head lane. Co-Authored-By: Claude Opus 5 --- .github/workflows/ruby-core.yml | 56 +++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/.github/workflows/ruby-core.yml b/.github/workflows/ruby-core.yml index 68a5171b13be..3ff91a5d72e4 100644 --- a/.github/workflows/ruby-core.yml +++ b/.github/workflows/ruby-core.yml @@ -92,6 +92,61 @@ jobs: working-directory: ruby/ruby if: matrix.target == 'Bundler' + ruby_core_master: + name: ${{ matrix.suite.name }} on a ruby-core master build${{ matrix.box && ' (RUBY_BOX=1)' || '' }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + suite: + - { name: Rubygems, deps: setup, task: test:parallel } + - { name: Bundler, deps: spec:deps, task: spec:regular, rubyopt: --disable-gems } + box: [false, true] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ruby/ruby + path: ruby/ruby + ref: master + persist-credentials: false + - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 + with: + ruby-version: 3.4 + bundler: none + - name: Install libraries + run: | + set -x + sudo apt-get update -q || : + sudo apt-get install --no-install-recommends -q -y build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm-dev bison autoconf + - name: Build and install Ruby + run: | + export GNUMAKEFLAGS="-j$((1 + $(nproc)))" + ./autogen.sh + ./configure -C --disable-install-doc --prefix="$RUNNER_TEMP/ruby" + make + make install + echo "$RUNNER_TEMP/ruby/bin" >> "$GITHUB_PATH" + working-directory: ruby/ruby + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: rubygems + persist-credentials: false + - name: Install dependencies + run: | + ruby -v + bin/rake ${{ matrix.suite.deps }} + env: + RUBYOPT: ${{ matrix.suite.rubyopt }} + working-directory: rubygems + - name: Run Test + run: bin/rake ${{ matrix.suite.task }} + env: + RUBYOPT: ${{ matrix.suite.rubyopt }} + RUBY_BOX: ${{ matrix.box && '1' || '' }} + working-directory: rubygems + + timeout-minutes: 90 + all-pass: name: All ruby-core jobs pass @@ -99,6 +154,7 @@ jobs: needs: - ruby_core + - ruby_core_master runs-on: ubuntu-latest From 430ec00de6a7314b66593068b08a8facc3dce1c1 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 14 Sep 2026 18:46:51 +0900 Subject: [PATCH 06/10] Avoid -r for gems that need activation under Ruby::Box A command line or RUBYOPT -r bypasses the RubyGems Kernel#require override under RUBY_BOX=1, so the gem is never activated. The cargo tests require inside -e instead, and the BUNDLE_CLEAN native extension example, which needs RUBYOPT -r, is skipped. https://bugs.ruby-lang.org/issues/22295 Co-Authored-By: Claude Opus 5 --- spec/commands/install_spec.rb | 4 ++++ test/rubygems/test_gem_ext_cargo_builder.rb | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/spec/commands/install_spec.rb b/spec/commands/install_spec.rb index d4b88902e21e..b5e4a9aa76c7 100644 --- a/spec/commands/install_spec.rb +++ b/spec/commands/install_spec.rb @@ -2065,6 +2065,10 @@ def gem_make_out end it "preserves bundled native extensions when BUNDLE_CLEAN removes another gem" do + # The command-line/RUBYOPT -r bypasses gem activation under RUBY_BOX=1 + # (https://bugs.ruby-lang.org/issues/22295) + skip "-r cannot activate gems under Ruby::Box" if defined?(Ruby::Box) && Ruby::Box.enabled? + build_repo4 do build_gem "native_child", "1.0", &:add_c_extension build_gem "native_parent", "1.0" do |s| diff --git a/test/rubygems/test_gem_ext_cargo_builder.rb b/test/rubygems/test_gem_ext_cargo_builder.rb index 94673a478c4e..bf442125f275 100644 --- a/test/rubygems/test_gem_ext_cargo_builder.rb +++ b/test/rubygems/test_gem_ext_cargo_builder.rb @@ -111,7 +111,9 @@ def test_full_integration Open3.capture2e(*gem, "build", "rust_ruby_example.gemspec", "--output", built_gem) Open3.capture2e(*gem, "install", "--verbose", "--local", built_gem, *ARGV) - stdout_and_stderr_str, status = Open3.capture2e(env_for_subprocess, *ruby_with_rubygems_in_load_path, "-rrust_ruby_example", "-e", "puts 'Result: ' + RustRubyExample.reverse('hello world')") + # Require inside -e because -r bypasses gem activation under RUBY_BOX=1 + # (https://bugs.ruby-lang.org/issues/22295) + stdout_and_stderr_str, status = Open3.capture2e(env_for_subprocess, *ruby_with_rubygems_in_load_path, "-e", "require 'rust_ruby_example'; puts 'Result: ' + RustRubyExample.reverse('hello world')") assert status.success?, stdout_and_stderr_str assert_match "Result: #{"hello world".reverse}", stdout_and_stderr_str end @@ -134,7 +136,7 @@ def test_custom_name Open3.capture2e(*gem, "install", "--verbose", "--local", built_gem, *ARGV) end - stdout_and_stderr_str, status = Open3.capture2e(env_for_subprocess, *ruby_with_rubygems_in_load_path, "-rcustom_name", "-e", "puts 'Result: ' + CustomName.say_hello") + stdout_and_stderr_str, status = Open3.capture2e(env_for_subprocess, *ruby_with_rubygems_in_load_path, "-e", "require 'custom_name'; puts 'Result: ' + CustomName.say_hello") assert status.success?, stdout_and_stderr_str assert_match "Result: Hello world!", stdout_and_stderr_str From ca3a38f6032a1c15d07efb3523b899118460f172 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 14 Sep 2026 18:47:02 +0900 Subject: [PATCH 07/10] Scope the File.expand_path stub in shared_helpers_spec A blanket stub also caught the path lookups of a lazy require of rubygems/yaml_serializer, which crashed with Errno::ENOENT under RUBY_BOX=1 with the parallel workers. These examples only need bundler/setup faked. Co-Authored-By: Claude Opus 5 --- spec/bundler/shared_helpers_spec.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/spec/bundler/shared_helpers_spec.rb b/spec/bundler/shared_helpers_spec.rb index 1619b0a14a59..232da4143a24 100644 --- a/spec/bundler/shared_helpers_spec.rb +++ b/spec/bundler/shared_helpers_spec.rb @@ -387,7 +387,11 @@ before do ENV["RUBYOPT"] = "-r#{install_path}/bundler/setup" - allow(File).to receive(:expand_path).and_return("#{install_path}/bundler/setup") + # Only fake the resolution of bundler/setup itself. A blanket stub + # breaks unrelated RubyGems path lookups triggered lazily inside the + # example, see #set_rubyopt. + allow(File).to receive(:expand_path).and_call_original + allow(File).to receive(:expand_path).with("setup", anything).and_return("#{install_path}/bundler/setup") allow(Gem).to receive(:bin_path).and_return("#{install_path}/bundler/setup") end @@ -403,7 +407,8 @@ let(:install_path) { "/opt/ruby with space/lib" } before do - allow(File).to receive(:expand_path).and_return("#{install_path}/bundler/setup") + allow(File).to receive(:expand_path).and_call_original + allow(File).to receive(:expand_path).with("setup", anything).and_return("#{install_path}/bundler/setup") allow(Gem).to receive(:bin_path).and_return("#{install_path}/bundler/setup") end From c0d86f643666b6825df793940f0f78b5de9f52e9 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 14 Sep 2026 18:47:11 +0900 Subject: [PATCH 08/10] Correct the Ruby::Box note on the extension build subprocess The recursion is not in RbConfig.expand. Inside a box defined?($gvar) does not see assignments made in that box, so mkmf have_devel? never sees its own memo. https://bugs.ruby-lang.org/issues/22283 Co-Authored-By: Claude Opus 5 --- lib/rubygems/ext/builder.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb index b7e80d6b06bb..691bc2825da0 100644 --- a/lib/rubygems/ext/builder.rb +++ b/lib/rubygems/ext/builder.rb @@ -101,7 +101,9 @@ def self.run(command, results, command_name = nil, dir = Dir.pwd, env = {}) require "open3" # Set $SOURCE_DATE_EPOCH for the subprocess. - # Under Ruby::Box mkmf makes RbConfig.expand recurse until SystemStackError. + # Under Ruby::Box defined?($gvar) does not see assignments made inside the + # box, so mkmf have_devel? never memoizes and recurses until SystemStackError + # (https://bugs.ruby-lang.org/issues/22283). # Drop $RUBY_BOX last so no caller can restore it. build_env = { "SOURCE_DATE_EPOCH" => Gem.source_date_epoch_string }.merge(env).merge("RUBY_BOX" => nil) # A single-element command would be parsed as a shell command line, From f780cbd7649962e557b1eea938ed01aa9730a34d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 14 Sep 2026 18:47:22 +0900 Subject: [PATCH 09/10] Skip the content-addressed ABI pin example on prerelease Ruby Content-addressed gems pin required_ruby_version to ~> X.Y.0, which a prerelease Ruby such as 4.1.0.dev does not satisfy, so the gem is never resolved on ruby-head. Co-Authored-By: Claude Opus 5 --- spec/install/gemfile/content_addressable_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/install/gemfile/content_addressable_spec.rb b/spec/install/gemfile/content_addressable_spec.rb index 8493c89cc203..8b0a472413fc 100644 --- a/spec/install/gemfile/content_addressable_spec.rb +++ b/spec/install/gemfile/content_addressable_spec.rb @@ -509,6 +509,7 @@ RSpec.describe "bundle install with content-addressable gems invisible to pre-4.1 RubyGems clients", :compact_index, rubygems: ">= 4.1.0.a" do before do skip "Gem::ContentAddress not available" if ruby_core? + skip "A prerelease Ruby does not satisfy the ~> X.Y.0 ABI pin of content-addressed gems" if Gem.ruby_version.prerelease? end let(:current_abi) { "#{Gem.ruby_version.segments[0]}.#{Gem.ruby_version.segments[1]}" } From ec7b4fb3a5a5063297a525dec2d2c0a831c73d45 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 15 Sep 2026 13:30:27 +0900 Subject: [PATCH 10/10] Skip ruby-core tests when ruby/ruby fails to build A broken ruby/ruby build is not something this repository can fix, so it should not fail the required check for every pull request. Co-Authored-By: Claude Opus 5 --- .github/workflows/ruby-core.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ruby-core.yml b/.github/workflows/ruby-core.yml index 3ff91a5d72e4..59a72ba11d65 100644 --- a/.github/workflows/ruby-core.yml +++ b/.github/workflows/ruby-core.yml @@ -40,12 +40,19 @@ jobs: sudo apt-get update -q || : sudo apt-get install --no-install-recommends -q -y build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm-dev bison autoconf - name: Build Ruby + id: build run: | export GNUMAKEFLAGS="-j$((1 + $(nproc)))" ./autogen.sh ./configure -C --disable-install-doc make working-directory: ruby/ruby + continue-on-error: true + - name: Skip tests when ruby/ruby does not build + run: echo "::warning::ruby/ruby ${BRANCH} failed to build, so the tests were skipped" + env: + BRANCH: ${{ matrix.branch }} + if: steps.build.outcome == 'failure' - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: rubygems/rubygems @@ -84,13 +91,13 @@ jobs: - name: Test RubyGems run: make -s test-all TESTS="--no-retry -j$((1 + $(nproc)))" working-directory: ruby/ruby - if: matrix.target == 'Rubygems' + if: steps.build.outcome == 'success' && matrix.target == 'Rubygems' - name: Test Bundler run: | git add . make test-bundler-parallel working-directory: ruby/ruby - if: matrix.target == 'Bundler' + if: steps.build.outcome == 'success' && matrix.target == 'Bundler' ruby_core_master: name: ${{ matrix.suite.name }} on a ruby-core master build${{ matrix.box && ' (RUBY_BOX=1)' || '' }} @@ -119,6 +126,7 @@ jobs: sudo apt-get update -q || : sudo apt-get install --no-install-recommends -q -y build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm-dev bison autoconf - name: Build and install Ruby + id: build run: | export GNUMAKEFLAGS="-j$((1 + $(nproc)))" ./autogen.sh @@ -127,6 +135,10 @@ jobs: make install echo "$RUNNER_TEMP/ruby/bin" >> "$GITHUB_PATH" working-directory: ruby/ruby + continue-on-error: true + - name: Skip tests when ruby/ruby does not build + run: echo "::warning::ruby/ruby master failed to build, so the tests were skipped" + if: steps.build.outcome == 'failure' - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: rubygems @@ -138,12 +150,14 @@ jobs: env: RUBYOPT: ${{ matrix.suite.rubyopt }} working-directory: rubygems + if: steps.build.outcome == 'success' - name: Run Test run: bin/rake ${{ matrix.suite.task }} env: RUBYOPT: ${{ matrix.suite.rubyopt }} RUBY_BOX: ${{ matrix.box && '1' || '' }} working-directory: rubygems + if: steps.build.outcome == 'success' timeout-minutes: 90