diff --git a/.github/workflows/system-rubygems-bundler.yml b/.github/workflows/system-rubygems-bundler.yml index a9cafcc5a289..e6de96a32124 100644 --- a/.github/workflows/system-rubygems-bundler.yml +++ b/.github/workflows/system-rubygems-bundler.yml @@ -50,6 +50,8 @@ jobs: - name: Run Test run: | bin/parallel_rspec + env: + RGV: system - name: Save system RubyGems version to ENV run: | RGV=$(ruby -e 'puts Gem::VERSION.split(".")[0..2].join(".")') diff --git a/doc/GETTING_STARTED.md b/doc/GETTING_STARTED.md index a290ecd56f27..2585588fee0c 100644 --- a/doc/GETTING_STARTED.md +++ b/doc/GETTING_STARTED.md @@ -73,14 +73,18 @@ For realworld higher level specs (also run in CI): ## Developing Bundler and RubyGems Together -When developing Bundler features or bug fixes that require changes in RubyGems, you can set the `RGV` environment variable to point to the repository root so Bundler's test suite picks up those changes: +Bundler's test suite runs against the RubyGems version selected by the `RGV` environment variable. It defaults to `.`, the RubyGems in this repository, so no setup is needed for the usual case of developing Bundler and RubyGems together: - RGV=.. bin/parallel_rspec + bin/parallel_rspec You can also test against specific RubyGems versions: RGV=v3.2.33 bin/parallel_rspec +Or against the RubyGems copy shipped with your Ruby, as the system-rubygems-bundler CI job does: + + RGV=system bin/parallel_rspec + It's recommended to set this variable permanently using [direnv](https://direnv.net) for consistent development. ## Code Style and Quality diff --git a/lib/bundler/rubygems_gem_installer.rb b/lib/bundler/rubygems_gem_installer.rb index c6313ddf8d0e..d8c50556c531 100644 --- a/lib/bundler/rubygems_gem_installer.rb +++ b/lib/bundler/rubygems_gem_installer.rb @@ -94,6 +94,15 @@ def generate_plugins end end + # Reimplemented from RubyGems, since RubyGems older than 4.1 doesn't + # provide it + def remove_stale_plugins + return unless spec.plugins.empty? + + ensure_writable_dir @plugins_dir + remove_plugins_for(spec, @plugins_dir) + end + def warn_skipped_extensions return if spec.extensions.empty? diff --git a/lib/bundler/vendored_tsort.rb b/lib/bundler/vendored_tsort.rb index 8d8ab26ecef9..d7d680344efd 100644 --- a/lib/bundler/vendored_tsort.rb +++ b/lib/bundler/vendored_tsort.rb @@ -1,8 +1,21 @@ # frozen_string_literal: true -begin - require "rubygems/vendored_tsort" -rescue LoadError - require "tsort" - Gem::TSort = TSort +# The defined? guard avoids reopening Gem::TSort when an old RubyGems has +# already loaded its own copy, e.g. through rubygems/request_set from +# Gem.activate_bin_path in binstubs. +# +unless defined?(Gem::TSort) + begin + require "rubygems/vendored_tsort" + rescue LoadError + begin + # RubyGems 3.4 and 3.5 ship the same file under its pre-3.6 name. + # Requiring the real tsort here instead would activate the tsort + # default gem, and `bundler/setup` must not activate any gems. + require "rubygems/tsort" + rescue LoadError + require "tsort" + Gem::TSort = TSort + end + end end diff --git a/spec/commands/exec_spec.rb b/spec/commands/exec_spec.rb index d744fc616bf2..e7a294b42932 100644 --- a/spec/commands/exec_spec.rb +++ b/spec/commands/exec_spec.rb @@ -75,13 +75,13 @@ skip "https://github.com/ruby/rubygems/issues/3351" if mswin? install_gemfile "source \"https://gem.repo1\"; gem \"myrack\"" bundle "exec #{gem_cmd} --version" - expect(out).to eq(Gem::VERSION) + expect(out).to eq(exercised_rubygems_version.to_s) end it "works when exec'ing to rubygems through sh -c" do install_gemfile "source \"https://gem.repo1\"; gem \"myrack\"" bundle "exec sh -c '#{gem_cmd} --version'" - expect(out).to eq(Gem::VERSION) + expect(out).to eq(exercised_rubygems_version.to_s) end it "works when exec'ing back to bundler to run a remote resolve" do diff --git a/spec/commands/install_spec.rb b/spec/commands/install_spec.rb index f8a134f23108..1e0e3a7c244b 100644 --- a/spec/commands/install_spec.rb +++ b/spec/commands/install_spec.rb @@ -1394,7 +1394,7 @@ def run expect(gem_make_out).not_to include("make -j8") end - it "uses 3 slots from the available pool when running the compilation of an extension" do + it "uses 3 slots from the available pool when running the compilation of an extension", rubygems: ">= 4.1.0.dev" do ENV.delete("MAKEFLAGS") install_gemfile(<<~G, env: { "BUNDLE_JOBS" => "8" }) @@ -1407,7 +1407,7 @@ def run expect(gem_make_out).to include("make -j3") end - it "consumes 3 slots from the pool when BUNDLE_JOBS isn't set" do + it "consumes 3 slots from the pool when BUNDLE_JOBS isn't set", rubygems: ">= 4.1.0.dev" do ENV.delete("MAKEFLAGS") install_gemfile(<<~G) diff --git a/spec/install/gemfile/git_spec.rb b/spec/install/gemfile/git_spec.rb index ee4c91a4f2b4..5fa740f4e1c6 100644 --- a/spec/install/gemfile/git_spec.rb +++ b/spec/install/gemfile/git_spec.rb @@ -114,9 +114,12 @@ sha = git.ref_for("main", 11) spec_file = default_bundle_path("bundler/gems/foo-1.0-#{sha}/foo.gemspec") expect(spec_file).to exist - ruby_code = Gem::Specification.load(spec_file.to_s).to_ruby + # Serialize with the RubyGems that wrote the file, since `#to_ruby` + # output differs across RubyGems versions + ruby "print Gem::Specification.load(#{spec_file.to_s.dump}).to_ruby" + ruby_code = out file_code = File.read(spec_file) - expect(file_code).to eq(ruby_code) + expect(file_code.strip).to eq(ruby_code) end it "does not update the git source implicitly" do diff --git a/spec/install/gems/resolving_spec.rb b/spec/install/gems/resolving_spec.rb index 111d361aab9c..178f68e30bb6 100644 --- a/spec/install/gems/resolving_spec.rb +++ b/spec/install/gems/resolving_spec.rb @@ -726,7 +726,7 @@ Because every version of require_rubygems depends on RubyGems > 9000 and Gemfile depends on require_rubygems >= 0, RubyGems > 9000 is required. - So, because current RubyGems version is = #{Gem::VERSION}, + So, because current RubyGems version is = #{exercised_rubygems_version}, version solving has failed. E expect(err).to end_with(nice_error) diff --git a/spec/install/global_cache_spec.rb b/spec/install/global_cache_spec.rb index 259e8d57ba2e..5e5494dbe9d3 100644 --- a/spec/install/global_cache_spec.rb +++ b/spec/install/global_cache_spec.rb @@ -9,9 +9,10 @@ let(:source2) { "http://gemserver.example.org" } def cache_base - # Use the unified global gem cache path if available (from RubyGems), - # otherwise fall back to the Bundler-specific cache location - if Gem.respond_to?(:global_gem_cache_path) + # Use the unified global gem cache path if the RubyGems under test + # provides it, otherwise fall back to the Bundler-specific cache + # location that Bundler uses on RubyGems older than 4.0 + if exercised_rubygems_version >= Gem::Version.new("4.0.0.a") Pathname.new(Gem.global_gem_cache_path) else home(".bundle", "cache", "gems") diff --git a/spec/runtime/self_management_spec.rb b/spec/runtime/self_management_spec.rb index 176c2a312177..970a3869367f 100644 --- a/spec/runtime/self_management_spec.rb +++ b/spec/runtime/self_management_spec.rb @@ -171,11 +171,9 @@ expect(out).to eq(previous_minor) end - it "requires the right bundler version from the config and run bundle CLI without re-exec" do - unless Bundler.rubygems.provides?(">= 4.1.0.dev") - skip "This spec can only run when Gem::BundlerVersionFinder.bundler_versions reads bundler configs" - end - + # Can only run when Gem::BundlerVersionFinder.bundler_versions reads + # bundler configs + it "requires the right bundler version from the config and run bundle CLI without re-exec", rubygems: ">= 4.1.0.dev" do lockfile_bundled_with(current_version) bundle_config "version #{previous_minor}" diff --git a/spec/support/filters.rb b/spec/support/filters.rb index 2be25b4a78a6..67a32354d081 100644 --- a/spec/support/filters.rb +++ b/spec/support/filters.rb @@ -23,7 +23,10 @@ def inspect RSpec.configure do |config| config.filter_run_excluding realworld: true - config.filter_run_excluding rubygems: RequirementChecker.against(Gem.rubygems_version) + # Version-gated specs care about the RubyGems that spec-spawned processes + # run, which under RGV=system is older than the one loaded in this process. + exercised_rubygems_version = Gem::Version.new(ENV["BUNDLER_SPEC_SYSTEM_RUBYGEMS_VERSION"] || Gem::VERSION) + config.filter_run_excluding rubygems: RequirementChecker.against(exercised_rubygems_version) config.filter_run_excluding git: RequirementChecker.against(git_version) config.filter_run_excluding ruby_repo: !ENV["GEM_COMMAND"].nil? config.filter_run_excluding no_color_tty: Gem.win_platform? || !ENV["GITHUB_ACTION"].nil? diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb index b0d4b5008ba7..8798cca57bb5 100644 --- a/spec/support/helpers.rb +++ b/spec/support/helpers.rb @@ -183,9 +183,22 @@ def gembin(cmd, options = {}) sys_exec(cmd.to_s, options) end + # The RubyGems version that processes spawned by specs run. Under + # RGV=system it's the system RubyGems, which is older than the one loaded + # in the current process. + def exercised_rubygems_version + @exercised_rubygems_version ||= Gem::Version.new(ENV["BUNDLER_SPEC_SYSTEM_RUBYGEMS_VERSION"] || Gem::VERSION) + end + def sys_exec(cmd, options = {}, &block) env = options[:env] || {} - env["RUBYOPT"] = opt_add(opt_add("-r#{spec_dir}/support/switch_rubygems.rb", env["RUBYOPT"]), ENV["RUBYOPT"]) + if ENV["RGV"] == "system" + # Spawned processes must boot system RubyGems untouched, so don't make + # them switch, and drop the load path override the harness runs with. + env["RUBYOPT"] = opt_add(env["RUBYOPT"] || "", opt_remove("-I#{source_lib_dir}", ENV["RUBYOPT"])) + else + env["RUBYOPT"] = opt_add(opt_add("-r#{spec_dir}/support/switch_rubygems.rb", env["RUBYOPT"]), ENV["RUBYOPT"]) + end options[:env] = env sh(cmd, options, &block) diff --git a/spec/support/switch_rubygems.rb b/spec/support/switch_rubygems.rb index 640b9f83b7d1..0958a1e82775 100644 --- a/spec/support/switch_rubygems.rb +++ b/spec/support/switch_rubygems.rb @@ -2,4 +2,17 @@ require_relative "rubygems_version_manager" ENV["RGV"] ||= "." -RubygemsVersionManager.new(ENV["RGV"]).switch + +# RGV=system runs specs against system RubyGems: processes spawned by specs +# boot it untouched (see `Spec::Helpers#sys_exec`). The test harness itself +# still runs on the checked out RubyGems, because the repo's lib/ dir is +# unavoidably on its load path and would get mixed into an older RubyGems. +# The system RubyGems version is captured here, while it's still the one +# loaded, so version-gated specs can check the version actually under test. +source = ENV["RGV"] +if source == "system" + ENV["BUNDLER_SPEC_SYSTEM_RUBYGEMS_VERSION"] ||= Gem::VERSION + source = "." +end + +RubygemsVersionManager.new(source).switch