From 38d67986b044ec32681aabc3d9017c5bdb4e7289 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 08:36:24 +0000 Subject: [PATCH 01/10] Bump actions/cache in /.github/actions/setup/directories Bumps [actions/cache](https://github.com/actions/cache) from 5.0.0 to 5.0.1. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/a7833574556fa59680c1b7cb190c1735db73ebf0...9255dc7a253b0ccc959486e2bca901246202afeb) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 5.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/actions/setup/directories/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup/directories/action.yml b/.github/actions/setup/directories/action.yml index 21cc817cba81cb..1f0463c9ca021f 100644 --- a/.github/actions/setup/directories/action.yml +++ b/.github/actions/setup/directories/action.yml @@ -100,7 +100,7 @@ runs: path: ${{ inputs.srcdir }} fetch-depth: ${{ inputs.fetch-depth }} - - uses: actions/cache@a7833574556fa59680c1b7cb190c1735db73ebf0 # v5.0.0 + - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: ${{ inputs.srcdir }}/.downloaded-cache key: ${{ runner.os }}-${{ runner.arch }}-downloaded-cache From 060199910afeccd4c81f90beebbd406799c05c46 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 16 Dec 2025 11:09:34 +0900 Subject: [PATCH 02/10] [ruby/rubygems] Allow to show cli_help with bundler executable https://github.com/ruby/rubygems/commit/a091e3fd10 --- lib/bundler/cli.rb | 2 +- spec/bundler/bundler/cli_spec.rb | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index 17d8c42e6ecdc5..1f6a65ca57aedd 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -104,7 +104,7 @@ def cli_help primary_commands = ["install", "update", "cache", "exec", "config", "help"] list = self.class.printable_commands(true) - by_name = list.group_by {|name, _message| name.match(/^bundle (\w+)/)[1] } + by_name = list.group_by {|name, _message| name.match(/^bundler? (\w+)/)[1] } utilities = by_name.keys.sort - primary_commands primary_commands.map! {|name| (by_name[name] || raise("no primary command #{name}")).first } utilities.map! {|name| by_name[name].first } diff --git a/spec/bundler/bundler/cli_spec.rb b/spec/bundler/bundler/cli_spec.rb index 7bc8fd3d36adaf..e2c64b93940157 100644 --- a/spec/bundler/bundler/cli_spec.rb +++ b/spec/bundler/bundler/cli_spec.rb @@ -282,4 +282,15 @@ def out_with_macos_man_workaround bundler "--version" expect(out).to eq("#{Bundler::VERSION} (simulating Bundler 5)") end + + it "shows cli_help when bundler install and no Gemfile is found" do + bundler "install", raise_on_error: false + expect(err).to include("Could not locate Gemfile") + + expect(out).to include("Bundler version #{Bundler::VERSION}"). + and include("\n\nBundler commands:\n\n"). + and include("\n\n Primary commands:\n"). + and include("\n\n Utilities:\n"). + and include("\n\nOptions:\n") + end end From 3b3ab338516c9af009300761382f01a6beff4dd0 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Mon, 15 Dec 2025 22:18:44 -0500 Subject: [PATCH 03/10] ZJIT: Fix test failures from line number of `Primitive` shifting This can happen with documentation updates and we don't want those to trip on ZJIT tests. Redact the whole name since names like "_bi342" aren't that helpful anyways. --- zjit/src/hir.rs | 4 +++- zjit/src/hir/opt_tests.rs | 4 ++-- zjit/src/hir/tests.rs | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index dfb8bbaecb55b4..2f8f21225524d5 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1297,9 +1297,11 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Ok(()) } Insn::InvokeBuiltin { bf, args, leaf, .. } => { + let bf_name = unsafe { CStr::from_ptr(bf.name) }.to_str().unwrap(); write!(f, "InvokeBuiltin{} {}", if *leaf { " leaf" } else { "" }, - unsafe { CStr::from_ptr(bf.name) }.to_str().unwrap())?; + // e.g. Code that use `Primitive.cexpr!`. From BUILTIN_INLINE_PREFIX. + if bf_name.starts_with("_bi") { "" } else { bf_name })?; for arg in args { write!(f, ", {arg}")?; } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index e3c74c86364007..166d1d4754591e 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -2588,7 +2588,7 @@ mod hir_opt_tests { v13:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1000, zero?@0x1008, cme:0x1010) IncrCounter inline_iseq_optimized_send_count - v23:BasicObject = InvokeBuiltin leaf _bi285, v13 + v23:BasicObject = InvokeBuiltin leaf , v13 CheckInterrupts Return v23 "); @@ -2620,7 +2620,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1008, first@0x1010, cme:0x1018) PatchPoint NoSingletonClass(Array@0x1008) IncrCounter inline_iseq_optimized_send_count - v31:BasicObject = InvokeBuiltin leaf _bi132, v17 + v31:BasicObject = InvokeBuiltin leaf , v17 CheckInterrupts Return v31 "); diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 49f092337e9816..f67874fad7a832 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -3026,7 +3026,7 @@ pub mod hir_build_tests { EntryPoint JIT(0) Jump bb2(v4) bb2(v6:BasicObject): - v10:HeapObject = InvokeBuiltin leaf _bi20, v6 + v10:HeapObject = InvokeBuiltin leaf , v6 Jump bb3(v6, v10) bb3(v12:BasicObject, v13:HeapObject): CheckInterrupts @@ -3140,7 +3140,7 @@ pub mod hir_build_tests { EntryPoint JIT(0) Jump bb2(v4) bb2(v6:BasicObject): - v10:StringExact = InvokeBuiltin leaf _bi28, v6 + v10:StringExact = InvokeBuiltin leaf , v6 Jump bb3(v6, v10) bb3(v12:BasicObject, v13:StringExact): CheckInterrupts @@ -3162,7 +3162,7 @@ pub mod hir_build_tests { EntryPoint JIT(0) Jump bb2(v4) bb2(v6:BasicObject): - v10:StringExact = InvokeBuiltin leaf _bi12, v6 + v10:StringExact = InvokeBuiltin leaf , v6 Jump bb3(v6, v10) bb3(v12:BasicObject, v13:StringExact): CheckInterrupts From 3b50f4ba41ececd01dcf2e35c4071495f250d609 Mon Sep 17 00:00:00 2001 From: hituzi no sippo Date: Thu, 11 Dec 2025 19:16:17 +0900 Subject: [PATCH 04/10] [ruby/rubygems] Support single quotes in mise format ruby version https://github.com/ruby/rubygems/commit/a7d7ab39dd --- lib/bundler/ruby_dsl.rb | 8 ++++---- spec/bundler/bundler/ruby_dsl_spec.rb | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/bundler/ruby_dsl.rb b/lib/bundler/ruby_dsl.rb index db4d5521e54925..228904850a9295 100644 --- a/lib/bundler/ruby_dsl.rb +++ b/lib/bundler/ruby_dsl.rb @@ -42,16 +42,16 @@ def ruby(*ruby_version) # Loads the file relative to the dirname of the Gemfile itself. def normalize_ruby_file(filename) file_content = Bundler.read_file(gemfile.dirname.join(filename)) - # match "ruby-3.2.2", ruby = "3.2.2" or "ruby 3.2.2" capturing version string up to the first space or comment + # match "ruby-3.2.2", ruby = "3.2.2", ruby = '3.2.2' or "ruby 3.2.2" capturing version string up to the first space or comment if /^ # Start of line ruby # Literal "ruby" [\s-]* # Optional whitespace or hyphens (for "ruby-3.2.2" format) (?:=\s*)? # Optional equals sign with whitespace (for ruby = "3.2.2" format) - "? # Optional opening quote + ["']? # Optional opening quote ( # Start capturing group - [^\s#"]+ # One or more chars that aren't spaces, #, or quotes + [^\s#"']+ # One or more chars that aren't spaces, #, or quotes ) # End capturing group - "? # Optional closing quote + ["']? # Optional closing quote /x.match(file_content) $1 else diff --git a/spec/bundler/bundler/ruby_dsl_spec.rb b/spec/bundler/bundler/ruby_dsl_spec.rb index 0d02542fb595b3..4ef02966953dc2 100644 --- a/spec/bundler/bundler/ruby_dsl_spec.rb +++ b/spec/bundler/bundler/ruby_dsl_spec.rb @@ -178,11 +178,21 @@ class MockDSL let(:file_content) do <<~TOML [tools] - ruby = "#{version}" + ruby = #{quote}#{version}#{quote} TOML end - it_behaves_like "it stores the ruby version" + context "with double quotes" do + let(:quote) { '"' } + + it_behaves_like "it stores the ruby version" + end + + context "with single quotes" do + let(:quote) { "'" } + + it_behaves_like "it stores the ruby version" + end end context "with a .tool-versions file format" do From f3b9509b52bbf845d95bf799d76dad41783919e5 Mon Sep 17 00:00:00 2001 From: hituzi no sippo Date: Sat, 13 Dec 2025 15:53:10 +0900 Subject: [PATCH 05/10] [ruby/rubygems] Fix quote handling in mise format ruby version parsing The previous regex didn't properly match quoted strings it would capture the opening quote as part of the version if quotes were mismatched. This change properly parses double-quoted, single-quoted, and unquoted version strings separately. https://github.com/ruby/rubygems/commit/81e48c8185 --- lib/bundler/ruby_dsl.rb | 25 ++++++++++++++----------- spec/bundler/bundler/ruby_dsl_spec.rb | 13 +++++++++++++ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/lib/bundler/ruby_dsl.rb b/lib/bundler/ruby_dsl.rb index 228904850a9295..5e52f38c8fd719 100644 --- a/lib/bundler/ruby_dsl.rb +++ b/lib/bundler/ruby_dsl.rb @@ -43,17 +43,20 @@ def ruby(*ruby_version) def normalize_ruby_file(filename) file_content = Bundler.read_file(gemfile.dirname.join(filename)) # match "ruby-3.2.2", ruby = "3.2.2", ruby = '3.2.2' or "ruby 3.2.2" capturing version string up to the first space or comment - if /^ # Start of line - ruby # Literal "ruby" - [\s-]* # Optional whitespace or hyphens (for "ruby-3.2.2" format) - (?:=\s*)? # Optional equals sign with whitespace (for ruby = "3.2.2" format) - ["']? # Optional opening quote - ( # Start capturing group - [^\s#"']+ # One or more chars that aren't spaces, #, or quotes - ) # End capturing group - ["']? # Optional closing quote - /x.match(file_content) - $1 + version_match = /^ # Start of line + ruby # Literal "ruby" + [\s-]* # Optional whitespace or hyphens (for "ruby-3.2.2" format) + (?:=\s*)? # Optional equals sign with whitespace (for ruby = "3.2.2" format) + (?: + "([^"]+)" # Double quoted version + | + '([^']+)' # Single quoted version + | + ([^\s#"']+) # Unquoted version + ) + /x.match(file_content) + if version_match + version_match[1] || version_match[2] || version_match[3] else file_content.strip end diff --git a/spec/bundler/bundler/ruby_dsl_spec.rb b/spec/bundler/bundler/ruby_dsl_spec.rb index 4ef02966953dc2..45a37c5795c873 100644 --- a/spec/bundler/bundler/ruby_dsl_spec.rb +++ b/spec/bundler/bundler/ruby_dsl_spec.rb @@ -193,6 +193,19 @@ class MockDSL it_behaves_like "it stores the ruby version" end + + context "with mismatched quotes" do + let(:file_content) do + <<~TOML + [tools] + ruby = "#{version}' + TOML + end + + it "raises an error" do + expect { subject }.to raise_error(Bundler::InvalidArgumentError, "= is not a valid requirement on the Ruby version") + end + end end context "with a .tool-versions file format" do From 9168cad4d63a5d281d443bde4edea6be213b0b25 Mon Sep 17 00:00:00 2001 From: Randy Stauner Date: Thu, 11 Dec 2025 15:56:16 -0700 Subject: [PATCH 06/10] YJIT: Bail out if proc would be stored above stack top Fixes [Bug #21266]. --- bootstraptest/test_yjit.rb | 10 ++++++++++ test/ruby/test_yjit.rb | 12 ++++++++++++ yjit/src/codegen.rs | 5 +++++ yjit/src/stats.rs | 1 + 4 files changed, 28 insertions(+) diff --git a/bootstraptest/test_yjit.rb b/bootstraptest/test_yjit.rb index cf1605cf407ac3..be66395190b273 100644 --- a/bootstraptest/test_yjit.rb +++ b/bootstraptest/test_yjit.rb @@ -4889,6 +4889,16 @@ def tests tests } +# regression test for splat with &proc{} when the target has rest (Bug #21266) +assert_equal '[]', %q{ + def foo(args) = bar(*args, &proc { _1 }) + def bar(_, _, _, _, *rest) = yield rest + + GC.stress = true + foo([1,2,3,4]) + foo([1,2,3,4]) +} + # regression test for invalidating an empty block assert_equal '0', %q{ def foo = (* = 1).pred diff --git a/test/ruby/test_yjit.rb b/test/ruby/test_yjit.rb index ce9dfca3900e0e..2096585451c324 100644 --- a/test/ruby/test_yjit.rb +++ b/test/ruby/test_yjit.rb @@ -1772,6 +1772,18 @@ def req2kws = yield a: 1, b: 2 RUBY end + def test_proc_block_with_kwrest + # When the bug was present this required --yjit-stats to trigger. + assert_compiles(<<~RUBY, result: {extra: 5}) + def foo = bar(w: 1, x: 2, y: 3, z: 4, extra: 5, &proc { _1 }) + def bar(w:, x:, y:, z:, **kwrest) = yield kwrest + + GC.stress = true + foo + foo + RUBY + end + private def code_gc_helpers diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs index 35d1713ed83ae9..620bdb82800a78 100644 --- a/yjit/src/codegen.rs +++ b/yjit/src/codegen.rs @@ -7896,6 +7896,11 @@ fn gen_send_iseq( gen_counter_incr(jit, asm, Counter::send_iseq_clobbering_block_arg); return None; } + if iseq_has_rest || has_kwrest { + // The proc would be stored above the current stack top, where GC can't see it + gen_counter_incr(jit, asm, Counter::send_iseq_block_arg_gc_unsafe); + return None; + } let proc = asm.stack_pop(1); // Pop first, as argc doesn't account for the block arg let callee_specval = asm.ctx.sp_opnd(callee_specval); asm.store(callee_specval, proc); diff --git a/yjit/src/stats.rs b/yjit/src/stats.rs index 4f23d97bce7360..84549fa5d34963 100644 --- a/yjit/src/stats.rs +++ b/yjit/src/stats.rs @@ -354,6 +354,7 @@ make_counters! { send_iseq_arity_error, send_iseq_block_arg_type, send_iseq_clobbering_block_arg, + send_iseq_block_arg_gc_unsafe, send_iseq_complex_discard_extras, send_iseq_leaf_builtin_block_arg_block_param, send_iseq_kw_splat_non_nil, From 9f593156b6184718a65f99905e6b07002058ebe6 Mon Sep 17 00:00:00 2001 From: Edouard CHIN Date: Mon, 8 Dec 2025 22:02:03 +0100 Subject: [PATCH 07/10] [ruby/rubygems] Pass down value of `BUNDLE_JOBS` to RubyGems before compiling: - ### Problem Since https://github.com/ruby/rubygems/pull/9131, we are now compiling make rules simultaneously. The number of jobs is equal to the number of processors. This may be problematic for some users as they want to control this value. ### Solution The number of jobs passed to `make` will now be equal to the `BUNDLE_JOBS` value. ### Side note It's also worth to note that since Bundler installs gems in parallel, we may end up running multiple `make -j` in parallel which would cause exhaust the number of processors we have. This problem can be fixed by implementing a GNU jobserver, which I plan to do. But I felt that this would be too much change in one PR. https://github.com/ruby/rubygems/commit/d51995deb9 --- lib/bundler/rubygems_gem_installer.rb | 4 ++ lib/rubygems/ext/builder.rb | 19 +++++-- lib/rubygems/ext/cargo_builder.rb | 2 +- lib/rubygems/ext/cmake_builder.rb | 2 +- lib/rubygems/ext/configure_builder.rb | 4 +- lib/rubygems/ext/ext_conf_builder.rb | 7 +-- lib/rubygems/ext/rake_builder.rb | 2 +- lib/rubygems/installer.rb | 7 ++- spec/bundler/commands/install_spec.rb | 73 +++++++++++++++++++++++++++ 9 files changed, 105 insertions(+), 15 deletions(-) diff --git a/lib/bundler/rubygems_gem_installer.rb b/lib/bundler/rubygems_gem_installer.rb index 0da5ed236b2bdf..64ce6193d3d1f5 100644 --- a/lib/bundler/rubygems_gem_installer.rb +++ b/lib/bundler/rubygems_gem_installer.rb @@ -103,6 +103,10 @@ def generate_bin_script(filename, bindir) end end + def build_jobs + Bundler.settings[:jobs] || super + end + def build_extensions extension_cache_path = options[:bundler_extension_cache_path] extension_dir = spec.extension_dir diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb index 600a6a5ff675ac..350daf1e16d7b7 100644 --- a/lib/rubygems/ext/builder.rb +++ b/lib/rubygems/ext/builder.rb @@ -22,7 +22,7 @@ def self.class_name end def self.make(dest_path, results, make_dir = Dir.pwd, sitedir = nil, targets = ["clean", "", "install"], - target_rbconfig: Gem.target_rbconfig) + target_rbconfig: Gem.target_rbconfig, n_jobs: nil) unless File.exist? File.join(make_dir, "Makefile") # No makefile exists, nothing to do. raise NoMakefileError, "No Makefile found in #{make_dir}" @@ -34,8 +34,18 @@ def self.make(dest_path, results, make_dir = Dir.pwd, sitedir = nil, targets = [ make_program_name ||= RUBY_PLATFORM.include?("mswin") ? "nmake" : "make" make_program = shellsplit(make_program_name) + is_nmake = /\bnmake/i.match?(make_program_name) # The installation of the bundled gems is failed when DESTDIR is empty in mswin platform. - destdir = /\bnmake/i !~ make_program_name || ENV["DESTDIR"] && ENV["DESTDIR"] != "" ? format("DESTDIR=%s", ENV["DESTDIR"]) : "" + destdir = !is_nmake || ENV["DESTDIR"] && ENV["DESTDIR"] != "" ? format("DESTDIR=%s", ENV["DESTDIR"]) : "" + + # nmake doesn't support parallel build + unless is_nmake + have_make_arguments = make_program.size > 1 + + if !have_make_arguments && !ENV["MAKEFLAGS"] && n_jobs + make_program << "-j#{n_jobs}" + end + end env = [destdir] @@ -147,11 +157,12 @@ def self.shelljoin(command) # have build arguments, saved, set +build_args+ which is an ARGV-style # array. - def initialize(spec, build_args = spec.build_args, target_rbconfig = Gem.target_rbconfig) + def initialize(spec, build_args = spec.build_args, target_rbconfig = Gem.target_rbconfig, build_jobs = nil) @spec = spec @build_args = build_args @gem_dir = spec.full_gem_path @target_rbconfig = target_rbconfig + @build_jobs = build_jobs @ran_rake = false end @@ -208,7 +219,7 @@ def build_extension(extension, dest_path) # :nodoc: FileUtils.mkdir_p dest_path results = builder.build(extension, dest_path, - results, @build_args, lib_dir, extension_dir, @target_rbconfig) + results, @build_args, lib_dir, extension_dir, @target_rbconfig, n_jobs: @build_jobs) verbose { results.join("\n") } diff --git a/lib/rubygems/ext/cargo_builder.rb b/lib/rubygems/ext/cargo_builder.rb index 6bf3b405ad7ce4..42dca3b102546c 100644 --- a/lib/rubygems/ext/cargo_builder.rb +++ b/lib/rubygems/ext/cargo_builder.rb @@ -15,7 +15,7 @@ def initialize end def build(extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd, - target_rbconfig = Gem.target_rbconfig) + target_rbconfig = Gem.target_rbconfig, n_jobs: nil) require "tempfile" require "fileutils" diff --git a/lib/rubygems/ext/cmake_builder.rb b/lib/rubygems/ext/cmake_builder.rb index 2915568b39d0c1..e660ed558b7e8a 100644 --- a/lib/rubygems/ext/cmake_builder.rb +++ b/lib/rubygems/ext/cmake_builder.rb @@ -37,7 +37,7 @@ def initialize end def build(extension, dest_path, results, args = [], lib_dir = nil, cmake_dir = Dir.pwd, - target_rbconfig = Gem.target_rbconfig) + target_rbconfig = Gem.target_rbconfig, n_jobs: nil) if target_rbconfig.path warn "--target-rbconfig is not yet supported for CMake extensions. Ignoring" end diff --git a/lib/rubygems/ext/configure_builder.rb b/lib/rubygems/ext/configure_builder.rb index 76c1cd8b197548..230b214b3cb504 100644 --- a/lib/rubygems/ext/configure_builder.rb +++ b/lib/rubygems/ext/configure_builder.rb @@ -8,7 +8,7 @@ class Gem::Ext::ConfigureBuilder < Gem::Ext::Builder def self.build(extension, dest_path, results, args = [], lib_dir = nil, configure_dir = Dir.pwd, - target_rbconfig = Gem.target_rbconfig) + target_rbconfig = Gem.target_rbconfig, n_jobs: nil) if target_rbconfig.path warn "--target-rbconfig is not yet supported for configure-based extensions. Ignoring" end @@ -19,7 +19,7 @@ def self.build(extension, dest_path, results, args = [], lib_dir = nil, configur run cmd, results, class_name, configure_dir end - make dest_path, results, configure_dir, target_rbconfig: target_rbconfig + make dest_path, results, configure_dir, target_rbconfig: target_rbconfig, n_jobs: n_jobs results end diff --git a/lib/rubygems/ext/ext_conf_builder.rb b/lib/rubygems/ext/ext_conf_builder.rb index 81491eac79abbc..822454355d104d 100644 --- a/lib/rubygems/ext/ext_conf_builder.rb +++ b/lib/rubygems/ext/ext_conf_builder.rb @@ -8,7 +8,7 @@ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder def self.build(extension, dest_path, results, args = [], lib_dir = nil, extension_dir = Dir.pwd, - target_rbconfig = Gem.target_rbconfig) + target_rbconfig = Gem.target_rbconfig, n_jobs: nil) require "fileutils" require "tempfile" @@ -40,11 +40,8 @@ def self.build(extension, dest_path, results, args = [], lib_dir = nil, extensio end ENV["DESTDIR"] = nil - unless RUBY_PLATFORM.include?("mswin") && RbConfig::CONFIG["configure_args"]&.include?("nmake") - ENV["MAKEFLAGS"] ||= "-j#{Etc.nprocessors + 1}" - end - make dest_path, results, extension_dir, tmp_dest_relative, target_rbconfig: target_rbconfig + make dest_path, results, extension_dir, tmp_dest_relative, target_rbconfig: target_rbconfig, n_jobs: n_jobs full_tmp_dest = File.join(extension_dir, tmp_dest_relative) diff --git a/lib/rubygems/ext/rake_builder.rb b/lib/rubygems/ext/rake_builder.rb index 0eac5a180ca3ca..d702d7f3393757 100644 --- a/lib/rubygems/ext/rake_builder.rb +++ b/lib/rubygems/ext/rake_builder.rb @@ -8,7 +8,7 @@ class Gem::Ext::RakeBuilder < Gem::Ext::Builder def self.build(extension, dest_path, results, args = [], lib_dir = nil, extension_dir = Dir.pwd, - target_rbconfig = Gem.target_rbconfig) + target_rbconfig = Gem.target_rbconfig, n_jobs: nil) if target_rbconfig.path warn "--target-rbconfig is not yet supported for Rake extensions. Ignoring" end diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 4c3038770d486b..90aa25dc072888 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -635,6 +635,7 @@ def process_options # :nodoc: @build_root = options[:build_root] @build_args = options[:build_args] + @build_jobs = options[:build_jobs] @gem_home = @install_dir || user_install_dir || Gem.dir @@ -803,7 +804,7 @@ def windows_stub_script(bindir, bin_file_name) # configure scripts and rakefiles or mkrf_conf files. def build_extensions - builder = Gem::Ext::Builder.new spec, build_args, Gem.target_rbconfig + builder = Gem::Ext::Builder.new spec, build_args, Gem.target_rbconfig, build_jobs builder.build_extensions end @@ -941,6 +942,10 @@ def build_args end end + def build_jobs + @build_jobs ||= Etc.nprocessors + 1 + end + def rb_config Gem.target_rbconfig end diff --git a/spec/bundler/commands/install_spec.rb b/spec/bundler/commands/install_spec.rb index 41b3a865cd9dfd..ae651bf981c7f8 100644 --- a/spec/bundler/commands/install_spec.rb +++ b/spec/bundler/commands/install_spec.rb @@ -1306,6 +1306,79 @@ def run end end + describe "parallel make" do + before do + unless Gem::Installer.private_method_defined?(:build_jobs) + skip "This example is runnable when RubyGems::Installer implements `build_jobs`" + end + + @old_makeflags = ENV["MAKEFLAGS"] + @gemspec = nil + + extconf_code = <<~CODE + require "mkmf" + create_makefile("foo") + CODE + + build_repo4 do + build_gem "mypsych", "4.0.6" do |s| + @gemspec = s + extension = "ext/mypsych/extconf.rb" + s.extensions = extension + + s.write(extension, extconf_code) + end + end + end + + after do + if @old_makeflags + ENV["MAKEFLAGS"] = @old_makeflags + else + ENV.delete("MAKEFLAGS") + end + end + + it "doesn't pass down -j to make when MAKEFLAGS is set" do + ENV["MAKEFLAGS"] = "-j1" + + install_gemfile(<<~G, env: { "BUNDLE_JOBS" => "8" }) + source "https://gem.repo4" + gem "mypsych" + G + + gem_make_out = File.read(File.join(@gemspec.extension_dir, "gem_make.out")) + + expect(gem_make_out).not_to include("make -j8") + end + + it "pass down the BUNDLE_JOBS to RubyGems when running the compilation of an extension" do + ENV.delete("MAKEFLAGS") + + install_gemfile(<<~G, env: { "BUNDLE_JOBS" => "8" }) + source "https://gem.repo4" + gem "mypsych" + G + + gem_make_out = File.read(File.join(@gemspec.extension_dir, "gem_make.out")) + + expect(gem_make_out).to include("make -j8") + end + + it "uses nprocessors by default" do + ENV.delete("MAKEFLAGS") + + install_gemfile(<<~G) + source "https://gem.repo4" + gem "mypsych" + G + + gem_make_out = File.read(File.join(@gemspec.extension_dir, "gem_make.out")) + + expect(gem_make_out).to include("make -j#{Etc.nprocessors + 1}") + end + end + describe "when configured path is UTF-8 and a file inside a gem package too" do let(:app_path) do path = tmp("♥") From 080bf30c48a8212cf0ee4f769d1bdaa4a2df5900 Mon Sep 17 00:00:00 2001 From: Edouard CHIN Date: Wed, 10 Dec 2025 14:29:32 +0100 Subject: [PATCH 08/10] [ruby/rubygems] Allow to specify the number of `make` jobs when installing gems: - Added a new `-j` option to `gem install` and `gem update`. This option allows to specify the number of jobs we pass to `make` when compiling gem with native extensions. By default its the number of processors, but users may want a way to control this. You can use it like so: `gem install json -j8` https://github.com/ruby/rubygems/commit/67aad88ca6 --- lib/rubygems/dependency_installer.rb | 2 ++ lib/rubygems/install_update_options.rb | 9 ++++++ .../test_gem_commands_install_command.rb | 28 +++++++++++++++++++ .../test_gem_commands_update_command.rb | 28 +++++++++++++++++++ .../test_gem_install_update_options.rb | 12 ++++++++ 5 files changed, 79 insertions(+) diff --git a/lib/rubygems/dependency_installer.rb b/lib/rubygems/dependency_installer.rb index a6cfc3c07af76d..6a6dfa5c2031ba 100644 --- a/lib/rubygems/dependency_installer.rb +++ b/lib/rubygems/dependency_installer.rb @@ -83,6 +83,7 @@ def initialize(options = {}) @user_install = options[:user_install] @wrappers = options[:wrappers] @build_args = options[:build_args] + @build_jobs = options[:build_jobs] @build_docs_in_background = options[:build_docs_in_background] @dir_mode = options[:dir_mode] @data_mode = options[:data_mode] @@ -154,6 +155,7 @@ def install(dep_or_name, version = Gem::Requirement.default) options = { bin_dir: @bin_dir, build_args: @build_args, + build_jobs: @build_jobs, document: @document, env_shebang: @env_shebang, force: @force, diff --git a/lib/rubygems/install_update_options.rb b/lib/rubygems/install_update_options.rb index 2d80e997879715..66cb5c049bcc03 100644 --- a/lib/rubygems/install_update_options.rb +++ b/lib/rubygems/install_update_options.rb @@ -31,6 +31,15 @@ def add_install_update_options options[:bin_dir] = File.expand_path(value) end + add_option(:"Install/Update", "-j", "--build-jobs VALUE", Integer, + "Specify the number of jobs to pass to `make` when installing", + "gems with native extensions.", + "Defaults to the number of processors.", + "This option is ignored on the mswin platform or", + "if the MAKEFLAGS environment variable is set.") do |value, options| + options[:build_jobs] = value + end + add_option(:"Install/Update", "--document [TYPES]", Array, "Generate documentation for installed gems", "List the documentation types you wish to", diff --git a/test/rubygems/test_gem_commands_install_command.rb b/test/rubygems/test_gem_commands_install_command.rb index 92933bfb77d06d..4fb7a04fb1c6d3 100644 --- a/test/rubygems/test_gem_commands_install_command.rb +++ b/test/rubygems/test_gem_commands_install_command.rb @@ -1584,6 +1584,34 @@ def test_suggest_update_if_enabled end end + def test_pass_down_the_job_option_to_make + gemspec = nil + + spec_fetcher do |fetcher| + fetcher.gem "a", 2 do |spec| + gemspec = spec + + extconf_path = "#{spec.gem_dir}/extconf.rb" + + write_file(extconf_path) do |io| + io.puts "require 'mkmf'" + io.puts "create_makefile '#{spec.name}'" + end + + spec.extensions = "extconf.rb" + end + end + + use_ui @ui do + assert_raise Gem::MockGemUi::SystemExitException, @ui.error do + @cmd.invoke "a", "-j4" + end + end + + gem_make_out = File.read(File.join(gemspec.extension_dir, "gem_make.out")) + assert_includes(gem_make_out, "make -j4") + end + def test_execute_bindir_with_nonexistent_parent_dirs spec_fetcher do |fetcher| fetcher.gem "a", 2 do |s| diff --git a/test/rubygems/test_gem_commands_update_command.rb b/test/rubygems/test_gem_commands_update_command.rb index 3b106e4581004f..9051640c0b1e9c 100644 --- a/test/rubygems/test_gem_commands_update_command.rb +++ b/test/rubygems/test_gem_commands_update_command.rb @@ -696,6 +696,34 @@ def test_fetch_remote_gems_prerelease assert_equal expected, @cmd.fetch_remote_gems(specs["a-1"]) end + def test_pass_down_the_job_option_to_make + gemspec = nil + + spec_fetcher do |fetcher| + fetcher.download "a", 3 do |spec| + gemspec = spec + + extconf_path = "#{spec.gem_dir}/extconf.rb" + + write_file(extconf_path) do |io| + io.puts "require 'mkmf'" + io.puts "create_makefile '#{spec.name}'" + end + + spec.extensions = "extconf.rb" + end + + fetcher.gem "a", 2 + end + + use_ui @ui do + @cmd.invoke("a", "-j2") + end + + gem_make_out = File.read(File.join(gemspec.extension_dir, "gem_make.out")) + assert_includes(gem_make_out, "make -j2") + end + def test_handle_options_system @cmd.handle_options %w[--system] diff --git a/test/rubygems/test_gem_install_update_options.rb b/test/rubygems/test_gem_install_update_options.rb index 8fd5d9c543babe..1e451dcb050f4d 100644 --- a/test/rubygems/test_gem_install_update_options.rb +++ b/test/rubygems/test_gem_install_update_options.rb @@ -202,4 +202,16 @@ def test_minimal_deps assert_equal true, @cmd.options[:minimal_deps] end + + def test_build_jobs_short_version + @cmd.handle_options %w[-j 4] + + assert_equal 4, @cmd.options[:build_jobs] + end + + def test_build_jobs_long_version + @cmd.handle_options %w[--build-jobs 4] + + assert_equal 4, @cmd.options[:build_jobs] + end end From e4797e93213365956a6cec142122866c8f0c9a51 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 16 Dec 2025 13:49:16 +0900 Subject: [PATCH 09/10] [ruby/rubygems] Reset MAKEFLAGS option for build jobs tests https://github.com/ruby/rubygems/commit/09e6031a11 --- test/rubygems/helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index 6e0be10ef53044..dc40f4ecb1f8ec 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -333,6 +333,7 @@ def setup ENV["XDG_CONFIG_HOME"] = nil ENV["XDG_DATA_HOME"] = nil ENV["XDG_STATE_HOME"] = nil + ENV["MAKEFLAGS"] = nil ENV["SOURCE_DATE_EPOCH"] = nil ENV["BUNDLER_VERSION"] = nil ENV["BUNDLE_CONFIG"] = nil From 5b0fefef417cad1733c18a0b66db07bcb1de5caf Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 16 Dec 2025 14:50:44 +0900 Subject: [PATCH 10/10] [ruby/rubygems] Added assertion for Windows and nmake https://github.com/ruby/rubygems/commit/be5c4e27d9 --- test/rubygems/test_gem_commands_install_command.rb | 6 +++++- test/rubygems/test_gem_commands_update_command.rb | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/test/rubygems/test_gem_commands_install_command.rb b/test/rubygems/test_gem_commands_install_command.rb index 4fb7a04fb1c6d3..72ca9d8262583a 100644 --- a/test/rubygems/test_gem_commands_install_command.rb +++ b/test/rubygems/test_gem_commands_install_command.rb @@ -1609,7 +1609,11 @@ def test_pass_down_the_job_option_to_make end gem_make_out = File.read(File.join(gemspec.extension_dir, "gem_make.out")) - assert_includes(gem_make_out, "make -j4") + if vc_windows? && nmake_found? + refute_includes(gem_make_out, "-j4") + else + assert_includes(gem_make_out, "make -j4") + end end def test_execute_bindir_with_nonexistent_parent_dirs diff --git a/test/rubygems/test_gem_commands_update_command.rb b/test/rubygems/test_gem_commands_update_command.rb index 9051640c0b1e9c..3bb4a72c4151ad 100644 --- a/test/rubygems/test_gem_commands_update_command.rb +++ b/test/rubygems/test_gem_commands_update_command.rb @@ -721,7 +721,11 @@ def test_pass_down_the_job_option_to_make end gem_make_out = File.read(File.join(gemspec.extension_dir, "gem_make.out")) - assert_includes(gem_make_out, "make -j2") + if vc_windows? && nmake_found? + refute_includes(gem_make_out, "-j2") + else + assert_includes(gem_make_out, "make -j2") + end end def test_handle_options_system