diff --git a/bundler/lib/bundler/man/bundle-config.1 b/bundler/lib/bundler/man/bundle-config.1 index 626f0811d249..8835ae75bd9e 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, 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, 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\. .TP diff --git a/bundler/lib/bundler/man/bundle-config.1.ronn b/bundler/lib/bundler/man/bundle-config.1.ronn index c01e836f96b5..9657e7414514 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, 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, 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 124058697873..e4dd2d95afa3 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,12 @@ def install generate_bin end - generate_plugins + if options[:install_plugin] == false + remove_stale_plugins + warn_skipped_plugins + else + generate_plugins + end write_spec @@ -80,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/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/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..15d6aac0fd1b 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -287,7 +287,12 @@ def install run_post_build_hooks generate_bin - generate_plugins + if options[:install_plugin] == false + remove_stale_plugins + warn_skipped_plugins + else + generate_plugins + end write_spec write_cache_file @@ -298,7 +303,7 @@ def install Gem::Specification.add_spec(spec) unless @install_dir - load_plugin + load_plugin unless options[:install_plugin] == false run_post_install_hooks @@ -804,11 +809,37 @@ 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 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 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 + + 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. # @@ -990,9 +1021,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 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/spec/install/gems/no_build_extension_spec.rb b/spec/install/gems/no_build_extension_spec.rb new file mode 100644 index 000000000000..31f01704339a --- /dev/null +++ b/spec/install/gems/no_build_extension_spec.rb @@ -0,0 +1,54 @@ +# 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 and warns 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 + 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 + 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 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..e040e6b813e8 --- /dev/null +++ b/spec/install/gems/no_install_plugin_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with --no-install-plugin" do + before do + build_repo2 do + 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 + + let(:plugin_path) { default_bundle_path("plugins", "with_plugin_plugin.rb") } + + 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 + source "https://gem.repo2" + gem "with_plugin", "1.0" + 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 + install_gemfile <<-G + source "https://gem.repo2" + gem "with_plugin", "1.0" + G + expect(plugin_path).to exist + + 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 + + expect(plugin_path).to exist + end +end 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 diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index f20771c5f02e..4cdc9479f722 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -2442,6 +2442,136 @@ 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 + assert_match "gem pristine #{@spec.name} --extensions", @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" + 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_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 + + installer.options[:install_plugin] = false + + build_rake_in do + use_ui @ui do + installer.install + end + end + + 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 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