From 8c9a971f28bc735e37e34e76f802f62f8242b7e4 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 2 Apr 2026 12:33:55 +0900 Subject: [PATCH 01/12] Add --[no-]build-extension and --[no-]install-plugin options to gem install These options allow users to opt out of building native extensions and installing plugins during gem installation, providing an equivalent to npm's --ignore-scripts for mitigating arbitrary code execution vectors. Both options default to true to maintain backward compatibility. Users can disable them per-command or globally via gemrc configuration. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/rubygems/dependency_installer.rb | 4 ++ lib/rubygems/install_update_options.rb | 12 ++++++ lib/rubygems/installer.rb | 15 +++++++- lib/rubygems/request_set.rb | 2 +- test/rubygems/test_gem_installer.rb | 52 ++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 3 deletions(-) diff --git a/lib/rubygems/dependency_installer.rb b/lib/rubygems/dependency_installer.rb index 6a6dfa5c2031..c842714d9580 100644 --- a/lib/rubygems/dependency_installer.rb +++ b/lib/rubygems/dependency_installer.rb @@ -88,6 +88,8 @@ def initialize(options = {}) @dir_mode = options[:dir_mode] @data_mode = options[:data_mode] @prog_mode = options[:prog_mode] + @build_extension = options[:build_extension] + @install_plugin = options[:install_plugin] # Indicates that we should not try to update any deps unless # we absolutely must. @@ -169,6 +171,8 @@ def install(dep_or_name, version = Gem::Requirement.default) dir_mode: @dir_mode, data_mode: @data_mode, prog_mode: @prog_mode, + build_extension: @build_extension, + install_plugin: @install_plugin, } options[:install_dir] = @install_dir if @only_install_dir diff --git a/lib/rubygems/install_update_options.rb b/lib/rubygems/install_update_options.rb index 66cb5c049bcc..e8859cadaf15 100644 --- a/lib/rubygems/install_update_options.rb +++ b/lib/rubygems/install_update_options.rb @@ -192,6 +192,18 @@ def add_install_update_options "rbconfig.rb for the deployment target platform") do |v, _o| Gem.set_target_rbconfig(v) end + + add_option(:"Install/Update", "--[no-]build-extension", + "Build native extensions during installation.", + "Defaults to true") do |v, _o| + options[:build_extension] = v + end + + add_option(:"Install/Update", "--[no-]install-plugin", + "Install plugins during installation.", + "Defaults to true") do |v, _o| + options[:install_plugin] = v + end end ## diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 914e41367731..730708fc3359 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -282,12 +282,16 @@ def install extract_files - build_extensions + if options[:build_extension] == false + warn_skipped_extensions + else + build_extensions + end write_build_info_file run_post_build_hooks generate_bin - generate_plugins + generate_plugins unless options[:install_plugin] == false write_spec write_cache_file @@ -809,6 +813,13 @@ def build_extensions builder.build_extensions end + def warn_skipped_extensions # :nodoc: + return if spec.extensions.empty? + + alert_warning "#{spec.full_name} contains native extensions that were not built.\n" \ + "To build extensions, run: gem install #{spec.name} --build-extension" + end + ## # Reads the file index and extracts each file into the gem directory. # diff --git a/lib/rubygems/request_set.rb b/lib/rubygems/request_set.rb index 5a855fdb1040..dbebd1af0c37 100644 --- a/lib/rubygems/request_set.rb +++ b/lib/rubygems/request_set.rb @@ -182,7 +182,7 @@ def install(options, &block) # :yields: request, installer # Install requested gems after they have been downloaded sorted_requests.each do |req| if req.installed? && @always_install.none? {|spec| spec == req.spec.spec } - req.spec.spec.build_extensions + req.spec.spec.build_extensions unless options[:build_extension] == false yield req, nil if block_given? next end diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index f20771c5f02e..ca0a82a94e11 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -2442,6 +2442,58 @@ def test_gem_attribute assert_kind_of(String, installer.gem) end + def test_install_no_build_extension + installer = util_setup_installer + + gemdir = File.join @gemhome, "gems", @spec.full_name + + installer.options[:build_extension] = false + + use_ui @ui do + installer.install + end + + assert_path_exist gemdir + assert_path_not_exist File.join(@spec.extension_dir, "gem.build_complete") + assert_match "contains native extensions that were not built", @ui.error + end + + def test_install_no_build_extension_without_extensions + spec = quick_gem "b", 2 + + util_build_gem spec + + installer = util_installer spec, @gemhome + installer.options[:build_extension] = false + + use_ui @ui do + installer.install + end + + refute_match "contains native extensions", @ui.error + end + + def test_install_no_install_plugin + installer = util_setup_installer do |spec| + write_file File.join(@tempdir, "lib", "rubygems_plugin.rb") do |io| + io.write "# do nothing" + end + + spec.files += %w[lib/rubygems_plugin.rb] + end + + installer.options[:install_plugin] = false + + build_rake_in do + use_ui @ui do + installer.install + end + end + + plugin_path = File.join Gem.plugindir, "a_plugin.rb" + refute File.exist?(plugin_path), "plugin must not be written when --no-install-plugin" + end + private def util_execless From 164abd12e047cea04c7b72da2b02da33cf017434 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 2 Apr 2026 12:34:43 +0900 Subject: [PATCH 02/12] Check plugin file existence before loading in load_plugin When plugins are not installed (e.g. via --no-install-plugin), the plugin files do not exist on disk. Without this check, load_plugin would attempt to load non-existent files and produce spurious LoadError warnings. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/rubygems/installer.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 730708fc3359..fc708f1aa69d 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -1001,9 +1001,10 @@ def load_plugin # are loaded at the same time. return unless specs.size == 1 - plugin_files = spec.plugins.map do |plugin| - File.join(@plugins_dir, "#{spec.name}_plugin#{File.extname(plugin)}") + plugin_files = spec.plugins.filter_map do |plugin| + path = File.join(@plugins_dir, "#{spec.name}_plugin#{File.extname(plugin)}") + path if File.exist?(path) end - Gem.load_plugin_files(plugin_files) + Gem.load_plugin_files(plugin_files) unless plugin_files.empty? end end From 0f31fc2a3106b8b7e061b387ad46c17903465f99 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 2 Apr 2026 13:27:35 +0900 Subject: [PATCH 03/12] Support no_build_extension and no_install_plugin settings in Bundler Extend the --no-build-extension and --no-install-plugin support to Bundler's installation paths. RubyGemsGemInstaller#install now respects these options, and the settings are propagated from Bundler::Settings through Source::RubyGems to the installer. Path::Installer also respects no_build_extension for git/path sources. Co-Authored-By: Claude Opus 4.6 (1M context) --- bundler/lib/bundler/rubygems_gem_installer.rb | 8 ++- bundler/lib/bundler/settings.rb | 2 + bundler/lib/bundler/source/path/installer.rb | 2 +- bundler/lib/bundler/source/rubygems.rb | 4 +- spec/install/gems/no_build_extension_spec.rb | 52 +++++++++++++++++++ 5 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 spec/install/gems/no_build_extension_spec.rb diff --git a/bundler/lib/bundler/rubygems_gem_installer.rb b/bundler/lib/bundler/rubygems_gem_installer.rb index 124058697873..dc904d43255e 100644 --- a/bundler/lib/bundler/rubygems_gem_installer.rb +++ b/bundler/lib/bundler/rubygems_gem_installer.rb @@ -27,7 +27,11 @@ def install extract_files end - build_extensions if spec.extensions.any? + if options[:build_extension] == false + warn_skipped_extensions + elsif spec.extensions.any? + build_extensions + end write_build_info_file run_post_build_hooks @@ -35,7 +39,7 @@ def install generate_bin end - generate_plugins + generate_plugins unless options[:install_plugin] == false write_spec diff --git a/bundler/lib/bundler/settings.rb b/bundler/lib/bundler/settings.rb index 120a3202afd5..95b48da31e92 100644 --- a/bundler/lib/bundler/settings.rb +++ b/bundler/lib/bundler/settings.rb @@ -30,7 +30,9 @@ class Settings init_gems_rb inline lockfile_checksums + no_build_extension no_install + no_install_plugin no_prune path.system plugins diff --git a/bundler/lib/bundler/source/path/installer.rb b/bundler/lib/bundler/source/path/installer.rb index 0af28fe77071..39765e5da229 100644 --- a/bundler/lib/bundler/source/path/installer.rb +++ b/bundler/lib/bundler/source/path/installer.rb @@ -24,7 +24,7 @@ def initialize(spec, options = {}) def post_install run_hooks(:pre_install) - unless @disable_extensions + unless @disable_extensions || Bundler.settings[:no_build_extension] build_extensions run_hooks(:post_build) end diff --git a/bundler/lib/bundler/source/rubygems.rb b/bundler/lib/bundler/source/rubygems.rb index cee5196f3082..b5c3b9169d16 100644 --- a/bundler/lib/bundler/source/rubygems.rb +++ b/bundler/lib/bundler/source/rubygems.rb @@ -533,7 +533,9 @@ def rubygems_gem_installer(spec, options) wrappers: true, env_shebang: true, build_args: options[:build_args], - bundler_extension_cache_path: extension_cache_path(spec) + bundler_extension_cache_path: extension_cache_path(spec), + build_extension: Bundler.settings[:no_build_extension] ? false : nil, + install_plugin: Bundler.settings[:no_install_plugin] ? false : nil ) @gem_installers_mutex.synchronize { @gem_installers[spec.name] ||= installer } end diff --git a/spec/install/gems/no_build_extension_spec.rb b/spec/install/gems/no_build_extension_spec.rb new file mode 100644 index 000000000000..38b570ef90f8 --- /dev/null +++ b/spec/install/gems/no_build_extension_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with --no-build-extension" do + before do + build_repo2 do + build_gem "with_extension" do |s| + s.extensions << "Rakefile" + s.write "Rakefile", <<-RUBY + task :default do + path = File.expand_path("lib", __dir__) + FileUtils.mkdir_p(path) + File.open("\#{path}/with_extension.rb", "w") do |f| + f.puts "WITH_EXTENSION = 'YES'" + end + end + RUBY + end + end + end + + it "skips building native extensions when no_build_extension is set" do + bundle_config "no_build_extension true" + + gemfile <<-G + source "https://gem.repo2" + gem "with_extension" + gem "rake" + G + + bundle :install + + build_complete = default_bundle_path("extensions").join( + Gem::Platform.local.to_s, + Gem.extension_api_version.to_s, + "with_extension-1.0", + "gem.build_complete" + ) + expect(build_complete).not_to exist + end + + it "builds native extensions by default" do + gemfile <<-G + source "https://gem.repo2" + gem "with_extension" + gem "rake" + G + + bundle :install + + expect(out).to include("Installing with_extension 1.0 with native extensions") + end +end From cceecbf63d5451f2097bea78b5019721fa33175f Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 9 Apr 2026 09:40:17 +0900 Subject: [PATCH 04/12] Add warnings with recovery instructions when skipping extensions or plugins When extensions or plugins are skipped via --no-build-extension or --no-install-plugin, warn the user and point them to the appropriate gem pristine command to re-enable later. Co-Authored-By: Claude Opus 4.6 (1M context) --- bundler/lib/bundler/rubygems_gem_installer.rb | 6 +++++- lib/rubygems/installer.rb | 15 +++++++++++++-- test/rubygems/test_gem_installer.rb | 17 +++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/bundler/lib/bundler/rubygems_gem_installer.rb b/bundler/lib/bundler/rubygems_gem_installer.rb index dc904d43255e..c6d49e133aef 100644 --- a/bundler/lib/bundler/rubygems_gem_installer.rb +++ b/bundler/lib/bundler/rubygems_gem_installer.rb @@ -39,7 +39,11 @@ def install generate_bin end - generate_plugins unless options[:install_plugin] == false + if options[:install_plugin] == false + warn_skipped_plugins + else + generate_plugins + end write_spec diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index fc708f1aa69d..6baa47fa17d0 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -291,7 +291,11 @@ def install run_post_build_hooks generate_bin - generate_plugins unless options[:install_plugin] == false + if options[:install_plugin] == false + warn_skipped_plugins + else + generate_plugins + end write_spec write_cache_file @@ -817,7 +821,14 @@ def warn_skipped_extensions # :nodoc: return if spec.extensions.empty? alert_warning "#{spec.full_name} contains native extensions that were not built.\n" \ - "To build extensions, run: gem install #{spec.name} --build-extension" + "To build extensions, run: gem pristine #{spec.name} --extensions" + end + + def warn_skipped_plugins # :nodoc: + return if spec.plugins.empty? + + alert_warning "#{spec.full_name} contains plugins that were not installed.\n" \ + "To install plugins, run: gem pristine #{spec.name} --only-plugins" end ## diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index ca0a82a94e11..424c13a1b742 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -2456,6 +2456,7 @@ def test_install_no_build_extension assert_path_exist gemdir assert_path_not_exist File.join(@spec.extension_dir, "gem.build_complete") assert_match "contains native extensions that were not built", @ui.error + assert_match "gem pristine #{@spec.name} --extensions", @ui.error end def test_install_no_build_extension_without_extensions @@ -2492,6 +2493,22 @@ def test_install_no_install_plugin plugin_path = File.join Gem.plugindir, "a_plugin.rb" refute File.exist?(plugin_path), "plugin must not be written when --no-install-plugin" + assert_match "contains plugins that were not installed", @ui.error + assert_match "gem pristine #{@spec.name} --only-plugins", @ui.error + end + + def test_install_no_install_plugin_without_plugins + installer = util_setup_installer + + installer.options[:install_plugin] = false + + build_rake_in do + use_ui @ui do + installer.install + end + end + + refute_match "contains plugins", @ui.error end private From 10e2749e9f9f792f24cb7f4554a8714070180e83 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 9 Apr 2026 09:54:26 +0900 Subject: [PATCH 05/12] Skip load_plugin when --no-install-plugin is specified Without this change, reinstalling or upgrading a gem with --no-install-plugin would still execute a pre-existing plugin wrapper left by a previous install via load_plugin. This defeats the opt-out. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/rubygems/installer.rb | 2 +- test/rubygems/test_gem_installer.rb | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 6baa47fa17d0..b27dea601f10 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -306,7 +306,7 @@ def install Gem::Specification.add_spec(spec) unless @install_dir - load_plugin + load_plugin unless options[:install_plugin] == false run_post_install_hooks diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index 424c13a1b742..083f9bf3e864 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -2497,6 +2497,34 @@ def test_install_no_install_plugin assert_match "gem pristine #{@spec.name} --only-plugins", @ui.error end + def test_install_no_install_plugin_skips_load_plugin + installer = util_setup_installer do |spec| + write_file File.join(@tempdir, "lib", "rubygems_plugin.rb") do |io| + io.write "$no_install_plugin_test_loaded = true" + end + + spec.files += %w[lib/rubygems_plugin.rb] + end + + # Simulate a pre-existing plugin wrapper from a previous install + FileUtils.mkdir_p Gem.plugindir + plugin_path = File.join Gem.plugindir, "a_plugin.rb" + File.write(plugin_path, "require_relative '../../gems/#{@spec.full_name}/lib/rubygems_plugin'") + + installer.options[:install_plugin] = false + + build_rake_in do + use_ui @ui do + installer.install + end + end + + refute defined?($no_install_plugin_test_loaded) && $no_install_plugin_test_loaded, + "plugin must not be loaded when --no-install-plugin" + ensure + $no_install_plugin_test_loaded = nil + end + def test_install_no_install_plugin_without_plugins installer = util_setup_installer From 3589a8b2e74a7f7d923ddac0ae2d11f5d8ccbd49 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 13 Apr 2026 09:32:36 +0900 Subject: [PATCH 06/12] Document no_build_extension and no_install_plugin settings in bundle-config The quality spec requires all Bundler settings to be documented in the bundle-config man page. Co-Authored-By: Claude Opus 4.6 (1M context) --- bundler/lib/bundler/man/bundle-config.1 | 6 ++++++ bundler/lib/bundler/man/bundle-config.1.ronn | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/bundler/lib/bundler/man/bundle-config.1 b/bundler/lib/bundler/man/bundle-config.1 index 626f0811d249..ca312cfe0020 100644 --- a/bundler/lib/bundler/man/bundle-config.1 +++ b/bundler/lib/bundler/man/bundle-config.1 @@ -154,9 +154,15 @@ The path to the lockfile that bundler should use\. By default, Bundler adds \fB\ \fBlockfile_checksums\fR (\fBBUNDLE_LOCKFILE_CHECKSUMS\fR) Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources\. Defaults to true\. .TP +\fBno_build_extension\fR (\fBBUNDLE_NO_BUILD_EXTENSION\fR) +Whether Bundler should skip building native extensions during installation\. When set, gems are installed without compiling their C extensions\. To build extensions later, run \fBgem pristine \-\-extensions\fR\. +.TP \fBno_install\fR (\fBBUNDLE_NO_INSTALL\fR) Whether \fBbundle package\fR should skip installing gems\. .TP +\fBno_install_plugin\fR (\fBBUNDLE_NO_INSTALL_PLUGIN\fR) +Whether Bundler should skip installing RubyGems plugins during installation\. When set, plugin files are not written to the plugins directory\. To install plugins later, run \fBgem pristine \-\-only\-plugins\fR\. +.TP \fBno_prune\fR (\fBBUNDLE_NO_PRUNE\fR) Whether Bundler should leave outdated gems unpruned when caching\. .TP diff --git a/bundler/lib/bundler/man/bundle-config.1.ronn b/bundler/lib/bundler/man/bundle-config.1.ronn index c01e836f96b5..1909bd7013fe 100644 --- a/bundler/lib/bundler/man/bundle-config.1.ronn +++ b/bundler/lib/bundler/man/bundle-config.1.ronn @@ -200,8 +200,16 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). Gemfile to disable lockfile creation entirely (see gemfile(5)). * `lockfile_checksums` (`BUNDLE_LOCKFILE_CHECKSUMS`): Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources. Defaults to true. +* `no_build_extension` (`BUNDLE_NO_BUILD_EXTENSION`): + Whether Bundler should skip building native extensions during installation. + When set, gems are installed without compiling their C extensions. + To build extensions later, run `gem pristine --extensions`. * `no_install` (`BUNDLE_NO_INSTALL`): Whether `bundle package` should skip installing gems. +* `no_install_plugin` (`BUNDLE_NO_INSTALL_PLUGIN`): + Whether Bundler should skip installing RubyGems plugins during installation. + When set, plugin files are not written to the plugins directory. + To install plugins later, run `gem pristine --only-plugins`. * `no_prune` (`BUNDLE_NO_PRUNE`): Whether Bundler should leave outdated gems unpruned when caching. * `only` (`BUNDLE_ONLY`): From 24b00a1e9ffb8ca7d33883799955e766fb2765e7 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 13 Apr 2026 09:36:31 +0900 Subject: [PATCH 07/12] Add Bundler spec for no_install_plugin setting Co-Authored-By: Claude Opus 4.6 (1M context) --- spec/install/gems/no_install_plugin_spec.rb | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 spec/install/gems/no_install_plugin_spec.rb diff --git a/spec/install/gems/no_install_plugin_spec.rb b/spec/install/gems/no_install_plugin_spec.rb new file mode 100644 index 000000000000..ca9df18b5179 --- /dev/null +++ b/spec/install/gems/no_install_plugin_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with --no-install-plugin" do + before do + build_repo2 do + build_gem "with_plugin" do |s| + s.write "lib/rubygems_plugin.rb", "# plugin code" + end + end + end + + it "skips installing plugins when no_install_plugin is set" do + bundle_config "no_install_plugin true" + + gemfile <<-G + source "https://gem.repo2" + gem "with_plugin" + G + + bundle :install + + plugin_path = default_bundle_path("plugins", "with_plugin_plugin.rb") + expect(plugin_path).not_to exist + end + + it "installs plugins by default" do + gemfile <<-G + source "https://gem.repo2" + gem "with_plugin" + G + + bundle :install + + plugin_path = default_bundle_path("plugins", "with_plugin_plugin.rb") + expect(plugin_path).to exist + end +end From 047a4380ea082998eb366001d1fa0927c1030d0d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 13 Apr 2026 09:37:53 +0900 Subject: [PATCH 08/12] Remove stale plugin wrappers even when --no-install-plugin is specified When a gem upgrades from a version with plugins to one without, generate_plugins normally removes the old wrapper files. Skipping generate_plugins entirely with --no-install-plugin prevented this cleanup, leaving stale wrappers that would still be loaded. Co-Authored-By: Claude Opus 4.6 (1M context) --- bundler/lib/bundler/rubygems_gem_installer.rb | 1 + lib/rubygems/installer.rb | 8 +++++ test/rubygems/test_gem_installer.rb | 33 +++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/bundler/lib/bundler/rubygems_gem_installer.rb b/bundler/lib/bundler/rubygems_gem_installer.rb index c6d49e133aef..ce7054b4db14 100644 --- a/bundler/lib/bundler/rubygems_gem_installer.rb +++ b/bundler/lib/bundler/rubygems_gem_installer.rb @@ -40,6 +40,7 @@ def install end if options[:install_plugin] == false + remove_stale_plugins warn_skipped_plugins else generate_plugins diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index b27dea601f10..c9b51e37aca9 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -292,6 +292,7 @@ def install generate_bin if options[:install_plugin] == false + remove_stale_plugins warn_skipped_plugins else generate_plugins @@ -831,6 +832,13 @@ def warn_skipped_plugins # :nodoc: "To install plugins, run: gem pristine #{spec.name} --only-plugins" end + def remove_stale_plugins # :nodoc: + return unless spec.plugins.empty? + + ensure_writable_dir @plugins_dir + remove_plugins_for(spec, @plugins_dir) + end + ## # Reads the file index and extracts each file into the gem directory. # diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index 083f9bf3e864..4cdc9479f722 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -2539,6 +2539,39 @@ def test_install_no_install_plugin_without_plugins refute_match "contains plugins", @ui.error end + def test_install_no_install_plugin_removes_stale_wrappers + # First install a version with a plugin + installer = util_setup_installer do |spec| + write_file File.join(@tempdir, "lib", "rubygems_plugin.rb") do |io| + io.write "# plugin code" + end + + spec.files += %w[lib/rubygems_plugin.rb] + end + + build_rake_in do + use_ui @ui do + installer.install + end + end + + plugin_path = File.join Gem.plugindir, "a_plugin.rb" + assert File.exist?(plugin_path), "plugin wrapper should exist after first install" + + # Now install a new version without plugins, using --no-install-plugin + spec2 = quick_gem "a", 3 + util_build_gem spec2 + + installer2 = util_installer spec2, @gemhome + installer2.options[:install_plugin] = false + + use_ui @ui do + installer2.install + end + + refute File.exist?(plugin_path), "stale plugin wrapper must be removed" + end + private def util_execless From 939287d9f5d1cc6c6e99f432b872738aca507f41 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 13 Apr 2026 09:39:19 +0900 Subject: [PATCH 09/12] Add no_build_extension and no_install_plugin specs to windows_tag_group Co-Authored-By: Claude Opus 4.6 (1M context) --- spec/support/windows_tag_group.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/support/windows_tag_group.rb b/spec/support/windows_tag_group.rb index f1a78f23e8d3..b91deb7ed3e6 100644 --- a/spec/support/windows_tag_group.rb +++ b/spec/support/windows_tag_group.rb @@ -187,6 +187,8 @@ module WindowsTagGroup "spec/bundler/digest_spec.rb", "spec/bundler/fetcher/gem_remote_fetcher_spec.rb", "spec/bundler/uri_normalizer_spec.rb", + "spec/install/gems/no_build_extension_spec.rb", + "spec/install/gems/no_install_plugin_spec.rb", ], }.freeze end From 336f2eb5baa22dbea2105d5f59563111793a07bf Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 16 Apr 2026 17:36:29 +0900 Subject: [PATCH 10/12] Honor --no-build-extension for git-sourced gems Gem::Resolver::GitSpecification#install calls Gem::Installer#build_extensions directly, so the guard at the main install path did not cover git sources. Move the options check into build_extensions itself so every caller skips the build and emits the same warning when the option is set. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/rubygems/installer.rb | 11 +++--- .../test_gem_resolver_git_specification.rb | 38 +++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index c9b51e37aca9..15d6aac0fd1b 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -282,11 +282,7 @@ def install extract_files - if options[:build_extension] == false - warn_skipped_extensions - else - build_extensions - end + build_extensions write_build_info_file run_post_build_hooks @@ -813,6 +809,11 @@ def windows_stub_script(bindir, bin_file_name) # configure scripts and rakefiles or mkrf_conf files. def build_extensions + if options[:build_extension] == false + warn_skipped_extensions + return + end + builder = Gem::Ext::Builder.new spec, build_args, Gem.target_rbconfig, build_jobs builder.build_extensions diff --git a/test/rubygems/test_gem_resolver_git_specification.rb b/test/rubygems/test_gem_resolver_git_specification.rb index 621333d3bf51..e03c61e27ddc 100644 --- a/test/rubygems/test_gem_resolver_git_specification.rb +++ b/test/rubygems/test_gem_resolver_git_specification.rb @@ -97,6 +97,44 @@ def test_install_extension assert_path_exist File.join git_spec.spec.extension_dir, "b.rb" end + def test_install_no_build_extension + pend if Gem.java_platform? + pend "terminates on mswin" if vc_windows? && ruby_repo? + name, _, repository, = git_gem "a", 1 do |s| + s.extensions << "ext/extconf.rb" + end + + Dir.chdir "git/a" do + FileUtils.mkdir_p "ext/lib" + + File.open "ext/extconf.rb", "w" do |io| + io.puts 'require "mkmf"' + io.puts 'create_makefile "a"' + end + + FileUtils.touch "ext/lib/b.rb" + + system @git, "add", "ext/extconf.rb" + system @git, "add", "ext/lib/b.rb" + + system @git, "commit", "--quiet", "-m", "Add extension files" + end + + source = Gem::Source::Git.new name, repository, nil, true + + spec = source.specs.first + + git_spec = Gem::Resolver::GitSpecification.new @set, spec, source + + use_ui @ui do + git_spec.install(build_extension: false) + end + + assert_path_not_exist File.join(git_spec.spec.extension_dir, "b.rb") + assert_match "contains native extensions that were not built", @ui.error + assert_match "gem pristine #{git_spec.spec.name} --extensions", @ui.error + end + def test_install_installed git_gem "a", 1 From 9ceb3206b17caf3a28d4854f3a93dd5a5cfa3130 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 16 Apr 2026 17:44:21 +0900 Subject: [PATCH 11/12] Cover stale plugin wrapper removal in no_install_plugin spec The existing spec only checked that the wrapper is skipped on a fresh install. Add a version upgrade case so that when a later version of the gem no longer ships plugins, the previously generated wrapper is removed even though no_install_plugin is set. Co-Authored-By: Claude Opus 4.6 (1M context) --- spec/install/gems/no_install_plugin_spec.rb | 38 ++++++++++++++------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/spec/install/gems/no_install_plugin_spec.rb b/spec/install/gems/no_install_plugin_spec.rb index ca9df18b5179..f3309eddbee7 100644 --- a/spec/install/gems/no_install_plugin_spec.rb +++ b/spec/install/gems/no_install_plugin_spec.rb @@ -3,35 +3,49 @@ RSpec.describe "bundle install with --no-install-plugin" do before do build_repo2 do - build_gem "with_plugin" do |s| + build_gem "with_plugin", "1.0" do |s| s.write "lib/rubygems_plugin.rb", "# plugin code" end + + build_gem "with_plugin", "2.0" end end - it "skips installing plugins when no_install_plugin is set" do + let(:plugin_path) { default_bundle_path("plugins", "with_plugin_plugin.rb") } + + it "does not generate the plugin wrapper when no_install_plugin is set" do bundle_config "no_install_plugin true" - gemfile <<-G + install_gemfile <<-G source "https://gem.repo2" - gem "with_plugin" + gem "with_plugin", "1.0" G - bundle :install - - plugin_path = default_bundle_path("plugins", "with_plugin_plugin.rb") expect(plugin_path).not_to exist end - it "installs plugins by default" do - gemfile <<-G + it "removes a stale plugin wrapper from a prior version when no_install_plugin is set" do + install_gemfile <<-G source "https://gem.repo2" - gem "with_plugin" + gem "with_plugin", "1.0" G + expect(plugin_path).to exist - bundle :install + bundle_config "no_install_plugin true" + install_gemfile <<-G + source "https://gem.repo2" + gem "with_plugin", "2.0" + G + + expect(plugin_path).not_to exist + end + + it "generates the plugin wrapper by default" do + install_gemfile <<-G + source "https://gem.repo2" + gem "with_plugin", "1.0" + G - plugin_path = default_bundle_path("plugins", "with_plugin_plugin.rb") expect(plugin_path).to exist end end From 46e626e9fe08c7dbf7f2890e8ea96f6e80cd9aa4 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 16 Apr 2026 18:03:14 +0900 Subject: [PATCH 12/12] Point recovery instructions at bundle pristine under Bundler gem pristine does not reach gems that Bundler installed under BUNDLE_PATH, so the guidance emitted when no_build_extension or no_install_plugin is set needs a Bundler-native equivalent. Override warn_skipped_extensions and warn_skipped_plugins in RubyGemsGemInstaller so they advise unsetting the matching Bundler setting and running bundle pristine, and update the bundle-config man page to match. Co-Authored-By: Claude Opus 4.6 (1M context) --- bundler/lib/bundler/man/bundle-config.1 | 4 ++-- bundler/lib/bundler/man/bundle-config.1.ronn | 4 ++-- bundler/lib/bundler/rubygems_gem_installer.rb | 14 ++++++++++++++ spec/install/gems/no_build_extension_spec.rb | 4 +++- spec/install/gems/no_install_plugin_spec.rb | 4 +++- 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/bundler/lib/bundler/man/bundle-config.1 b/bundler/lib/bundler/man/bundle-config.1 index ca312cfe0020..8835ae75bd9e 100644 --- a/bundler/lib/bundler/man/bundle-config.1 +++ b/bundler/lib/bundler/man/bundle-config.1 @@ -155,13 +155,13 @@ The path to the lockfile that bundler should use\. By default, Bundler adds \fB\ Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources\. Defaults to true\. .TP \fBno_build_extension\fR (\fBBUNDLE_NO_BUILD_EXTENSION\fR) -Whether Bundler should skip building native extensions during installation\. When set, gems are installed without compiling their C extensions\. To build extensions later, run \fBgem pristine \-\-extensions\fR\. +Whether Bundler should skip building native extensions during installation\. When set, gems are installed without compiling their C extensions\. To build extensions later, unset this setting and run \fBbundle pristine \fR\. .TP \fBno_install\fR (\fBBUNDLE_NO_INSTALL\fR) Whether \fBbundle package\fR should skip installing gems\. .TP \fBno_install_plugin\fR (\fBBUNDLE_NO_INSTALL_PLUGIN\fR) -Whether Bundler should skip installing RubyGems plugins during installation\. When set, plugin files are not written to the plugins directory\. To install plugins later, run \fBgem pristine \-\-only\-plugins\fR\. +Whether Bundler should skip installing RubyGems plugins during installation\. When set, plugin files are not written to the plugins directory\. To install plugins later, unset this setting and run \fBbundle pristine \fR\. .TP \fBno_prune\fR (\fBBUNDLE_NO_PRUNE\fR) Whether Bundler should leave outdated gems unpruned when caching\. diff --git a/bundler/lib/bundler/man/bundle-config.1.ronn b/bundler/lib/bundler/man/bundle-config.1.ronn index 1909bd7013fe..9657e7414514 100644 --- a/bundler/lib/bundler/man/bundle-config.1.ronn +++ b/bundler/lib/bundler/man/bundle-config.1.ronn @@ -203,13 +203,13 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). * `no_build_extension` (`BUNDLE_NO_BUILD_EXTENSION`): Whether Bundler should skip building native extensions during installation. When set, gems are installed without compiling their C extensions. - To build extensions later, run `gem pristine --extensions`. + To build extensions later, unset this setting and run `bundle pristine `. * `no_install` (`BUNDLE_NO_INSTALL`): Whether `bundle package` should skip installing gems. * `no_install_plugin` (`BUNDLE_NO_INSTALL_PLUGIN`): Whether Bundler should skip installing RubyGems plugins during installation. When set, plugin files are not written to the plugins directory. - To install plugins later, run `gem pristine --only-plugins`. + To install plugins later, unset this setting and run `bundle pristine `. * `no_prune` (`BUNDLE_NO_PRUNE`): Whether Bundler should leave outdated gems unpruned when caching. * `only` (`BUNDLE_ONLY`): diff --git a/bundler/lib/bundler/rubygems_gem_installer.rb b/bundler/lib/bundler/rubygems_gem_installer.rb index ce7054b4db14..e4dd2d95afa3 100644 --- a/bundler/lib/bundler/rubygems_gem_installer.rb +++ b/bundler/lib/bundler/rubygems_gem_installer.rb @@ -89,6 +89,20 @@ def generate_plugins end end + def warn_skipped_extensions + return if spec.extensions.empty? + + Bundler.ui.warn "#{spec.full_name} contains native extensions that were not built.\n" \ + "To build extensions, unset no_build_extension and run `bundle pristine #{spec.name}`." + end + + def warn_skipped_plugins + return if spec.plugins.empty? + + Bundler.ui.warn "#{spec.full_name} contains plugins that were not installed.\n" \ + "To install plugins, unset no_install_plugin and run `bundle pristine #{spec.name}`." + end + if Bundler.rubygems.provides?("< 3.5.19") def generate_bin_script(filename, bindir) bin_script_path = File.join bindir, formatted_program_filename(filename) diff --git a/spec/install/gems/no_build_extension_spec.rb b/spec/install/gems/no_build_extension_spec.rb index 38b570ef90f8..31f01704339a 100644 --- a/spec/install/gems/no_build_extension_spec.rb +++ b/spec/install/gems/no_build_extension_spec.rb @@ -18,7 +18,7 @@ end end - it "skips building native extensions when no_build_extension is set" do + it "skips building native extensions and warns when no_build_extension is set" do bundle_config "no_build_extension true" gemfile <<-G @@ -36,6 +36,8 @@ "gem.build_complete" ) expect(build_complete).not_to exist + expect(err).to include("with_extension-1.0 contains native extensions that were not built") + expect(err).to include("unset no_build_extension and run `bundle pristine with_extension`") end it "builds native extensions by default" do diff --git a/spec/install/gems/no_install_plugin_spec.rb b/spec/install/gems/no_install_plugin_spec.rb index f3309eddbee7..e040e6b813e8 100644 --- a/spec/install/gems/no_install_plugin_spec.rb +++ b/spec/install/gems/no_install_plugin_spec.rb @@ -13,7 +13,7 @@ let(:plugin_path) { default_bundle_path("plugins", "with_plugin_plugin.rb") } - it "does not generate the plugin wrapper when no_install_plugin is set" do + it "does not generate the plugin wrapper and warns when no_install_plugin is set" do bundle_config "no_install_plugin true" install_gemfile <<-G @@ -22,6 +22,8 @@ G expect(plugin_path).not_to exist + expect(err).to include("with_plugin-1.0 contains plugins that were not installed") + expect(err).to include("unset no_install_plugin and run `bundle pristine with_plugin`") end it "removes a stale plugin wrapper from a prior version when no_install_plugin is set" do