From 0c434ba2cd5cb6e31b00e22ee6b0716eaecc8440 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 18 Jun 2026 16:09:34 +0900 Subject: [PATCH 1/3] Skip the make jobserver on Windows The POSIX make jobserver passes tokens through an inherited pipe identified by file descriptor numbers, which Windows cannot inherit, and nmake aborts every native extension build with `fatal error U1065: invalid option '-'` when it finds the GNU make `--jobserver-auth` left in MAKEFLAGS. Skip the jobserver entirely on Windows so bundle install keeps working with nmake. https://github.com/ruby/rbs/actions/runs/27736519019/job/82054372031 Co-Authored-By: Claude Opus 4.8 --- .../bundler/installer/parallel_installer.rb | 41 +++++++++++-------- .../installer/parallel_installer_spec.rb | 20 +++++++++ 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/bundler/lib/bundler/installer/parallel_installer.rb b/bundler/lib/bundler/installer/parallel_installer.rb index 619ed14a7d4f..f7e2efa1f1a5 100644 --- a/bundler/lib/bundler/installer/parallel_installer.rb +++ b/bundler/lib/bundler/installer/parallel_installer.rb @@ -119,22 +119,31 @@ def install_with_worker end def with_jobserver - r, w = IO.pipe - r.close_on_exec = false - w.close_on_exec = false - w.write("*" * @size) - - old_makeflags = ENV["MAKEFLAGS"] - ENV["MAKEFLAGS"] = [old_makeflags, "--jobserver-auth=#{r.fileno},#{w.fileno}"].compact.join(" ") - - yield - ensure - # Restore MAKEFLAGS before closing the pipe so a close failure can't - # leave the process with descriptors that point at a closed pipe. - old_makeflags ? ENV["MAKEFLAGS"] = old_makeflags : ENV.delete("MAKEFLAGS") - - r&.close - w&.close + # The POSIX make jobserver hands tokens to child `make` processes through + # an inherited pipe identified by its file descriptor numbers, which + # Windows cannot inherit. nmake also rejects a GNU make `--jobserver-auth` + # left in MAKEFLAGS and aborts every native extension build with + # `fatal error U1065: invalid option '-'`. Skip the jobserver on Windows. + return yield if Gem.win_platform? + + begin + r, w = IO.pipe + r.close_on_exec = false + w.close_on_exec = false + w.write("*" * @size) + + old_makeflags = ENV["MAKEFLAGS"] + ENV["MAKEFLAGS"] = [old_makeflags, "--jobserver-auth=#{r.fileno},#{w.fileno}"].compact.join(" ") + + yield + ensure + # Restore MAKEFLAGS before closing the pipe so a close failure can't + # leave the process with descriptors that point at a closed pipe. + old_makeflags ? ENV["MAKEFLAGS"] = old_makeflags : ENV.delete("MAKEFLAGS") + + r&.close + w&.close + end end def install_serially diff --git a/spec/bundler/installer/parallel_installer_spec.rb b/spec/bundler/installer/parallel_installer_spec.rb index a2ec1429390d..8976913cab0e 100644 --- a/spec/bundler/installer/parallel_installer_spec.rb +++ b/spec/bundler/installer/parallel_installer_spec.rb @@ -219,4 +219,24 @@ def redefine_build_jobs Bundler::RubyGemsGemInstaller.define_method(:build_jobs, old_method) end end + + describe "make jobserver on Windows" do + # nmake reads MAKEFLAGS from the environment and treats its contents as + # bare option letters, so a GNU make `--jobserver-auth` aborts the build + # with `fatal error U1065: invalid option '-'`. The fd-based jobserver also + # cannot work on Windows, so it must be skipped there entirely. + it "leaves MAKEFLAGS untouched" do + allow(Gem).to receive(:win_platform?).and_return(true) + + parallel_installer = Bundler::ParallelInstaller.new(nil, [], 5, false, false) + + makeflags_before = ENV["MAKEFLAGS"] + makeflags_during = :not_yielded + parallel_installer.send(:with_jobserver) do + makeflags_during = ENV["MAKEFLAGS"] + end + + expect(makeflags_during).to eq(makeflags_before) + end + end end From 479d03dc265f102f95990100fcba3c5142adcdd5 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 18 Jun 2026 16:16:04 +0900 Subject: [PATCH 2/3] Run the jobserver Windows spec only on Windows The example exercises the genuine Windows code path, so tag it `windows_only` and exclude it elsewhere instead of stubbing `Gem.win_platform?`. Co-Authored-By: Claude Opus 4.8 --- spec/bundler/installer/parallel_installer_spec.rb | 4 +--- spec/support/filters.rb | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/bundler/installer/parallel_installer_spec.rb b/spec/bundler/installer/parallel_installer_spec.rb index 8976913cab0e..0016c4300db1 100644 --- a/spec/bundler/installer/parallel_installer_spec.rb +++ b/spec/bundler/installer/parallel_installer_spec.rb @@ -220,14 +220,12 @@ def redefine_build_jobs end end - describe "make jobserver on Windows" do + describe "make jobserver on Windows", :windows_only do # nmake reads MAKEFLAGS from the environment and treats its contents as # bare option letters, so a GNU make `--jobserver-auth` aborts the build # with `fatal error U1065: invalid option '-'`. The fd-based jobserver also # cannot work on Windows, so it must be skipped there entirely. it "leaves MAKEFLAGS untouched" do - allow(Gem).to receive(:win_platform?).and_return(true) - parallel_installer = Bundler::ParallelInstaller.new(nil, [], 5, false, false) makeflags_before = ENV["MAKEFLAGS"] diff --git a/spec/support/filters.rb b/spec/support/filters.rb index 2be25b4a78a6..03bc95925807 100644 --- a/spec/support/filters.rb +++ b/spec/support/filters.rb @@ -33,6 +33,7 @@ def inspect config.filter_run_excluding truffleruby_only: RUBY_ENGINE != "truffleruby" config.filter_run_excluding man: Gem.win_platform? config.filter_run_excluding mri_only: RUBY_ENGINE != "ruby" + config.filter_run_excluding windows_only: !Gem.win_platform? config.filter_run_when_matching :focus unless ENV["CI"] From b12db473deb47acca2ee3799232417107f9952d4 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 18 Jun 2026 16:39:40 +0900 Subject: [PATCH 3/3] Narrow the jobserver skip to nmake The first version skipped the jobserver on every Windows platform, but mingw uses GNU make and consumes the jobserver tokens through the inherited pipe without trouble. Only nmake on mswin reads MAKEFLAGS as bare option letters and rejects the GNU make `--jobserver-auth`. Detect nmake the way the extension builder does and skip the jobserver only then, so mingw keeps building in parallel. Co-Authored-By: Claude Opus 4.8 --- .../bundler/installer/parallel_installer.rb | 21 +++++++++++++------ .../installer/parallel_installer_spec.rb | 17 ++++++++++----- spec/support/filters.rb | 1 - 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/bundler/lib/bundler/installer/parallel_installer.rb b/bundler/lib/bundler/installer/parallel_installer.rb index f7e2efa1f1a5..f65f171cb8a8 100644 --- a/bundler/lib/bundler/installer/parallel_installer.rb +++ b/bundler/lib/bundler/installer/parallel_installer.rb @@ -119,12 +119,13 @@ def install_with_worker end def with_jobserver - # The POSIX make jobserver hands tokens to child `make` processes through - # an inherited pipe identified by its file descriptor numbers, which - # Windows cannot inherit. nmake also rejects a GNU make `--jobserver-auth` - # left in MAKEFLAGS and aborts every native extension build with - # `fatal error U1065: invalid option '-'`. Skip the jobserver on Windows. - return yield if Gem.win_platform? + # The jobserver hands tokens to child `make` processes through MAKEFLAGS + # using the GNU make `--jobserver-auth` protocol. nmake, the default make + # on mswin, instead reads MAKEFLAGS as bare option letters and aborts + # every native extension build with `fatal error U1065: invalid option + # '-'`. Skip the jobserver when nmake is in use. Other Windows toolchains + # such as mingw use GNU make and keep working through the inherited pipe. + return yield if nmake? begin r, w = IO.pipe @@ -146,6 +147,14 @@ def with_jobserver end end + # Mirror how RubyGems' extension builder picks the make program so the + # jobserver is only set up when a GNU-compatible make will consume it. + def nmake? + make = ENV["MAKE"] || ENV["make"] + make ||= "nmake" if RUBY_PLATFORM.include?("mswin") + /\bnmake/i.match?(make.to_s) + end + def install_serially until finished_installing? raise "failed to find a spec to enqueue while installing serially" unless spec_install = @specs.find(&:ready_to_enqueue?) diff --git a/spec/bundler/installer/parallel_installer_spec.rb b/spec/bundler/installer/parallel_installer_spec.rb index 0016c4300db1..6a91f05bf8b4 100644 --- a/spec/bundler/installer/parallel_installer_spec.rb +++ b/spec/bundler/installer/parallel_installer_spec.rb @@ -220,18 +220,25 @@ def redefine_build_jobs end end - describe "make jobserver on Windows", :windows_only do + describe "make jobserver with nmake" do # nmake reads MAKEFLAGS from the environment and treats its contents as # bare option letters, so a GNU make `--jobserver-auth` aborts the build - # with `fatal error U1065: invalid option '-'`. The fd-based jobserver also - # cannot work on Windows, so it must be skipped there entirely. + # with `fatal error U1065: invalid option '-'`. The jobserver must be + # skipped when nmake is the make program. it "leaves MAKEFLAGS untouched" do parallel_installer = Bundler::ParallelInstaller.new(nil, [], 5, false, false) makeflags_before = ENV["MAKEFLAGS"] makeflags_during = :not_yielded - parallel_installer.send(:with_jobserver) do - makeflags_during = ENV["MAKEFLAGS"] + + old_make = ENV["MAKE"] + ENV["MAKE"] = "nmake" + begin + parallel_installer.send(:with_jobserver) do + makeflags_during = ENV["MAKEFLAGS"] + end + ensure + ENV["MAKE"] = old_make end expect(makeflags_during).to eq(makeflags_before) diff --git a/spec/support/filters.rb b/spec/support/filters.rb index 03bc95925807..2be25b4a78a6 100644 --- a/spec/support/filters.rb +++ b/spec/support/filters.rb @@ -33,7 +33,6 @@ def inspect config.filter_run_excluding truffleruby_only: RUBY_ENGINE != "truffleruby" config.filter_run_excluding man: Gem.win_platform? config.filter_run_excluding mri_only: RUBY_ENGINE != "ruby" - config.filter_run_excluding windows_only: !Gem.win_platform? config.filter_run_when_matching :focus unless ENV["CI"]