From 560de053d1b86a4bfc0dd34a617392c0f38c3192 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 18:14:06 +0000 Subject: [PATCH 01/25] [ruby/rubygems] Bump the rb-sys group across 2 directories with 1 update Bumps the rb-sys group with 1 update in the /test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib directory: [rb-sys](https://github.com/oxidize-rb/rb-sys). Bumps the rb-sys group with 1 update in the /test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example directory: [rb-sys](https://github.com/oxidize-rb/rb-sys). Updates `rb-sys` from 0.9.125 to 0.9.127 - [Release notes](https://github.com/oxidize-rb/rb-sys/releases) - [Commits](https://github.com/oxidize-rb/rb-sys/compare/v0.9.125...v0.9.127) Updates `rb-sys` from 0.9.125 to 0.9.127 - [Release notes](https://github.com/oxidize-rb/rb-sys/releases) - [Commits](https://github.com/oxidize-rb/rb-sys/compare/v0.9.125...v0.9.127) --- updated-dependencies: - dependency-name: rb-sys dependency-version: 0.9.127 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rb-sys - dependency-name: rb-sys dependency-version: 0.9.127 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rb-sys ... Signed-off-by: dependabot[bot] https://github.com/ruby/rubygems/commit/79a46384f1 --- .../custom_name/ext/custom_name_lib/Cargo.lock | 8 ++++---- .../custom_name/ext/custom_name_lib/Cargo.toml | 2 +- .../rust_ruby_example/Cargo.lock | 8 ++++---- .../rust_ruby_example/Cargo.toml | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.lock b/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.lock index f96e796794e4fa..989b0b0412023e 100644 --- a/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.lock +++ b/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.lock @@ -153,18 +153,18 @@ dependencies = [ [[package]] name = "rb-sys" -version = "0.9.125" +version = "0.9.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b37650fabd8ba515910a0dc089dcb6348eb3c35fbf91698cb226435be2babc" +checksum = "d7d7c9560fe42dcffa576941394075f18a17dce89fcf718a2fa90b7dc2134d12" dependencies = [ "rb-sys-build", ] [[package]] name = "rb-sys-build" -version = "0.9.125" +version = "0.9.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c73b806faa66006e491458b48a78725621c1ac5a2a6efe2614c90711a7780b80" +checksum = "f1688e8f32967ba48c89e4dfa283b57f901075f542fc7ee9c3d7c5f9091ca1d9" dependencies = [ "bindgen", "lazy_static", diff --git a/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.toml b/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.toml index e9e21e0f4f8aba..47e978ceb4bc29 100644 --- a/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.toml +++ b/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.toml @@ -7,4 +7,4 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -rb-sys = "0.9.125" +rb-sys = "0.9.127" diff --git a/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock b/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock index 358d0f5a41fc45..e8ee8f237c5d69 100644 --- a/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock +++ b/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock @@ -146,18 +146,18 @@ dependencies = [ [[package]] name = "rb-sys" -version = "0.9.125" +version = "0.9.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b37650fabd8ba515910a0dc089dcb6348eb3c35fbf91698cb226435be2babc" +checksum = "d7d7c9560fe42dcffa576941394075f18a17dce89fcf718a2fa90b7dc2134d12" dependencies = [ "rb-sys-build", ] [[package]] name = "rb-sys-build" -version = "0.9.125" +version = "0.9.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c73b806faa66006e491458b48a78725621c1ac5a2a6efe2614c90711a7780b80" +checksum = "f1688e8f32967ba48c89e4dfa283b57f901075f542fc7ee9c3d7c5f9091ca1d9" dependencies = [ "bindgen", "lazy_static", diff --git a/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.toml b/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.toml index f9583c0e337c66..6595b6aafa6eee 100644 --- a/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.toml +++ b/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.toml @@ -7,4 +7,4 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -rb-sys = "0.9.125" +rb-sys = "0.9.127" From 2c5da6f237ef0622db030805348a1b4a5a0f7311 Mon Sep 17 00:00:00 2001 From: "Daisuke Fujimura (fd0)" Date: Mon, 4 May 2026 18:54:35 +0900 Subject: [PATCH 02/25] [ruby/rubygems] Remove cygwin from WIN_PATTERNS https://github.com/ruby/rubygems/commit/c01e3b9990 --- lib/rubygems/win_platform.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/rubygems/win_platform.rb b/lib/rubygems/win_platform.rb index 78e968fe493084..10556871b2a68e 100644 --- a/lib/rubygems/win_platform.rb +++ b/lib/rubygems/win_platform.rb @@ -8,7 +8,6 @@ module Gem WIN_PATTERNS = [ /bccwin/i, - /cygwin/i, /djgpp/i, /mingw/i, /mswin/i, From 4758d1b5f38a570752edeedd18c41d5383e802c6 Mon Sep 17 00:00:00 2001 From: Kazuhiro NISHIYAMA Date: Thu, 7 May 2026 17:45:32 +0900 Subject: [PATCH 03/25] Suppress a warning ``` compiling ../imemo.c ../imemo.c:563:20: warning: unused variable 'shape_id' [-Wunused-variable] 563 | shape_id_t shape_id = RBASIC_SHAPE_ID((VALUE)fields); | ^~~~~~~~ 1 warning generated. ``` --- imemo.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/imemo.c b/imemo.c index f236194cd27377..6027884f9bd631 100644 --- a/imemo.c +++ b/imemo.c @@ -560,8 +560,10 @@ static inline void imemo_fields_free(struct rb_fields *fields) { if (FL_TEST_RAW((VALUE)fields, OBJ_FIELD_HEAP)) { +#if RUBY_DEBUG shape_id_t shape_id = RBASIC_SHAPE_ID((VALUE)fields); RUBY_ASSERT(rb_shape_complex_p(shape_id)); +#endif st_free_table(fields->as.complex.table); } } From ff7ed1b345df902e42d414dad3cb6cc074849e64 Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Fri, 24 Apr 2026 05:26:56 +0900 Subject: [PATCH 04/25] [ruby/rubygems] Deprecate parsing non-lockfile content in LockfileParser Bundler::LockfileParser#initialize silently accepted any string input, including Gemfiles or arbitrary text, producing an empty parser with no indication that the input was invalid. This caused downstream tooling like bundler-audit to operate on unvalidated content. Detect non-lockfile content by checking for any of the known section headers; empty input is left untouched for backward compatibility. Rather than raising immediately, emit a deprecation warning via SharedHelpers.feature_deprecated! announcing that a future Bundler version will raise LockfileError. Expose LockfileParser#valid? so callers can branch on the result without string-matching the message. Fixes https://github.com/ruby/rubygems/pull/8932 https://github.com/ruby/rubygems/commit/7607fe4a5d --- lib/bundler/lockfile_parser.rb | 15 +++++ spec/bundler/bundler/lockfile_parser_spec.rb | 61 ++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/lib/bundler/lockfile_parser.rb b/lib/bundler/lockfile_parser.rb index a837f994cd90da..e0bb83ab666918 100644 --- a/lib/bundler/lockfile_parser.rb +++ b/lib/bundler/lockfile_parser.rb @@ -115,6 +115,17 @@ def initialize(lockfile, strict: false) "Run `git checkout HEAD -- #{@lockfile_path}` first to get a clean lock." end + @valid = lockfile.strip.empty? || + lockfile.split(/(?:\r?\n)+/).any? {|l| KNOWN_SECTIONS.include?(l) } + + unless @valid + SharedHelpers.feature_deprecated!( + "Your #{@lockfile_path} does not appear to be a valid lockfile. " \ + "Run `rm #{@lockfile_path}` and then `bundle install` to generate a new lockfile. " \ + "This will raise a LockfileError in a future version of Bundler." + ) + end + lockfile.split(/((?:\r?\n)+)/) do |line| # split alternates between the line and the following whitespace next @pos.advance!(line) if line.match?(/^\s*$/) @@ -164,6 +175,10 @@ def may_include_redundant_platform_specific_gems? bundler_version.nil? || bundler_version < Gem::Version.new("1.16.2") end + def valid? + @valid + end + private TYPES = { diff --git a/spec/bundler/bundler/lockfile_parser_spec.rb b/spec/bundler/bundler/lockfile_parser_spec.rb index f38da2c9932183..4b493a37576b52 100644 --- a/spec/bundler/bundler/lockfile_parser_spec.rb +++ b/spec/bundler/bundler/lockfile_parser_spec.rb @@ -129,6 +129,7 @@ shared_examples_for "parsing" do it "parses correctly" do + expect(subject.valid?).to be(true) expect(subject.sources).to eq sources expect(subject.dependencies).to eq dependencies expect(subject.specs).to eq specs @@ -191,6 +192,66 @@ include_examples "parsing" end + context "when the content does not contain any recognized lockfile sections" do + let(:lockfile_contents) { "hello world\nlorem ipsum\n" } + + it "does not raise, is not valid, and deprecates" do + expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with( + /does not appear to be a valid lockfile.*future version of Bundler/m + ) + parser = described_class.new(lockfile_contents) + expect(parser.valid?).to be(false) + expect(parser.specs).to eq([]) + expect(parser.dependencies).to eq({}) + end + + it "does not raise when strict: true, and still deprecates" do + expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with( + /does not appear to be a valid lockfile.*future version of Bundler/m + ) + parser = described_class.new(lockfile_contents, strict: true) + expect(parser.valid?).to be(false) + expect(parser.specs).to eq([]) + expect(parser.dependencies).to eq({}) + end + end + + context "when the content looks like a Gemfile DSL" do + let(:lockfile_contents) { <<~G } + source "https://rubygems.org" + gem "rake" + G + + it "does not raise, is not valid, and deprecates" do + expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with( + /does not appear to be a valid lockfile.*future version of Bundler/m + ) + parser = described_class.new(lockfile_contents) + expect(parser.valid?).to be(false) + expect(parser.specs).to eq([]) + expect(parser.dependencies).to eq({}) + end + + it "does not raise when strict: true, and still deprecates" do + expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with( + /does not appear to be a valid lockfile.*future version of Bundler/m + ) + parser = described_class.new(lockfile_contents, strict: true) + expect(parser.valid?).to be(false) + expect(parser.specs).to eq([]) + expect(parser.dependencies).to eq({}) + end + end + + context "when the content is empty" do + let(:lockfile_contents) { "" } + + it "does not raise and is valid" do + expect { subject }.not_to raise_error + expect(subject.valid?).to be(true) + end + end + context "when CHECKSUMS has duplicate checksums in the lockfile that don't match" do let(:bad_checksum) { "sha256=c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11" } let(:lockfile_contents) { super().split(/(?<=CHECKSUMS\n)/m).insert(1, " rake (10.3.2) #{bad_checksum}\n").join } From f7478ab13d2fcf17ecf8a1268d4c405ebadd6b9d Mon Sep 17 00:00:00 2001 From: Yuta Kurotaki Date: Fri, 1 May 2026 19:01:19 +0900 Subject: [PATCH 05/25] [ruby/rubygems] Fix RuboCop Layout/MultilineOperationIndentation offense https://github.com/ruby/rubygems/commit/2a47650f64 Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/bundler/lockfile_parser.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bundler/lockfile_parser.rb b/lib/bundler/lockfile_parser.rb index e0bb83ab666918..9e21f8f1d2a1ab 100644 --- a/lib/bundler/lockfile_parser.rb +++ b/lib/bundler/lockfile_parser.rb @@ -116,7 +116,7 @@ def initialize(lockfile, strict: false) end @valid = lockfile.strip.empty? || - lockfile.split(/(?:\r?\n)+/).any? {|l| KNOWN_SECTIONS.include?(l) } + lockfile.split(/(?:\r?\n)+/).any? {|l| KNOWN_SECTIONS.include?(l) } unless @valid SharedHelpers.feature_deprecated!( From a42a6bc2adca25070706688b3b8247d26160364f Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 1 May 2026 11:25:36 +0900 Subject: [PATCH 06/25] [ruby/rubygems] Add Bundler::Override value object Introduce a value object that holds a target/field/operation triple for the upcoming Gemfile `override` DSL. apply_to dispatches on the operation: a version spec string replaces the requirement absolutely, :ignore_upper strips < and <= while folding ~> into >=, and nil collapses to Gem::Requirement.default. The :ignore_upper logic is taken from Shopify/bundler-ignore-dependency. https://github.com/ruby/rubygems/commit/4c2cafa4e8 Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/bundler.rb | 1 + lib/bundler/override.rb | 46 +++++++++++++++ spec/bundler/bundler/override_spec.rb | 70 +++++++++++++++++++++++ spec/bundler/support/windows_tag_group.rb | 1 + 4 files changed, 118 insertions(+) create mode 100644 lib/bundler/override.rb create mode 100644 spec/bundler/bundler/override_spec.rb diff --git a/lib/bundler.rb b/lib/bundler.rb index 8bec10c0db8746..12dde90fc5d835 100644 --- a/lib/bundler.rb +++ b/lib/bundler.rb @@ -62,6 +62,7 @@ module Bundler autoload :MatchRemoteMetadata, File.expand_path("bundler/match_remote_metadata", __dir__) autoload :Materialization, File.expand_path("bundler/materialization", __dir__) autoload :NULL, File.expand_path("bundler/constants", __dir__) + autoload :Override, File.expand_path("bundler/override", __dir__) autoload :ProcessLock, File.expand_path("bundler/process_lock", __dir__) autoload :RemoteSpecification, File.expand_path("bundler/remote_specification", __dir__) autoload :Resolver, File.expand_path("bundler/resolver", __dir__) diff --git a/lib/bundler/override.rb b/lib/bundler/override.rb new file mode 100644 index 00000000000000..d07dad00ea6bf8 --- /dev/null +++ b/lib/bundler/override.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Bundler + class Override + LOWER_BOUND_OPERATORS = [">=", ">", "="].freeze + + attr_reader :target, :field, :operation + + def initialize(target, field, operation) + @target = target + @field = field + @operation = operation + end + + def apply_to(requirement) + case operation + when nil + Gem::Requirement.default + when :ignore_upper + remove_upper_bounds(requirement) + when String + Gem::Requirement.new(operation) + else + raise ArgumentError, "unsupported override operation: #{operation.inspect}" + end + end + + private + + def remove_upper_bounds(requirement) + return Gem::Requirement.default if requirement.nil? || requirement.none? + + lower_bounds = requirement.requirements.filter_map do |op, version| + if LOWER_BOUND_OPERATORS.include?(op) + [op, version] + elsif op == "~>" + [">=", version] + end + end + + return Gem::Requirement.default if lower_bounds.empty? + + Gem::Requirement.new(lower_bounds.map {|op, v| "#{op} #{v}" }) + end + end +end diff --git a/spec/bundler/bundler/override_spec.rb b/spec/bundler/bundler/override_spec.rb new file mode 100644 index 00000000000000..bc813e52fc3ff9 --- /dev/null +++ b/spec/bundler/bundler/override_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Override do + describe "#apply_to" do + context "when operation is a version spec string" do + it "replaces the existing requirement entirely" do + override = described_class.new("rails", :version, ">= 8.0") + result = override.apply_to(Gem::Requirement.new(">= 1.0", "< 2.0")) + expect(result).to eq(Gem::Requirement.new(">= 8.0")) + end + + it "ignores the existing requirement regardless of its content" do + override = described_class.new("rails", :version, "= 1.0") + result = override.apply_to(Gem::Requirement.new(">= 99.0")) + expect(result).to eq(Gem::Requirement.new("= 1.0")) + end + end + + context "when operation is :ignore_upper" do + it "removes < and <= operators" do + override = described_class.new("rails", :version, :ignore_upper) + result = override.apply_to(Gem::Requirement.new(">= 1.0", "< 2.0")) + expect(result).to eq(Gem::Requirement.new(">= 1.0")) + end + + it "keeps >, >=, = operators" do + override = described_class.new("rails", :version, :ignore_upper) + result = override.apply_to(Gem::Requirement.new("> 1.0", "<= 2.0")) + expect(result).to eq(Gem::Requirement.new("> 1.0")) + end + + it "converts ~> to >= preserving the lower bound" do + override = described_class.new("rails", :version, :ignore_upper) + result = override.apply_to(Gem::Requirement.new("~> 1.5")) + expect(result).to eq(Gem::Requirement.new(">= 1.5")) + end + + it "returns the default requirement when only upper bounds remain" do + override = described_class.new("rails", :version, :ignore_upper) + result = override.apply_to(Gem::Requirement.new("< 2.0")) + expect(result).to eq(Gem::Requirement.default) + end + + it "returns the default requirement when the input is nil" do + override = described_class.new("rails", :version, :ignore_upper) + expect(override.apply_to(nil)).to eq(Gem::Requirement.default) + end + + it "returns the default requirement when the input is already the default" do + override = described_class.new("rails", :version, :ignore_upper) + expect(override.apply_to(Gem::Requirement.default)).to eq(Gem::Requirement.default) + end + end + + context "when operation is nil" do + it "returns the default requirement" do + override = described_class.new("rails", :version, nil) + result = override.apply_to(Gem::Requirement.new(">= 1.0", "< 2.0")) + expect(result).to eq(Gem::Requirement.default) + end + end + + context "when operation is unsupported" do + it "raises ArgumentError" do + override = described_class.new("rails", :version, 42) + expect { override.apply_to(Gem::Requirement.default) }.to raise_error(ArgumentError, /unsupported override operation/) + end + end + end +end diff --git a/spec/bundler/support/windows_tag_group.rb b/spec/bundler/support/windows_tag_group.rb index b91deb7ed3e6f2..7b52b58743b61c 100644 --- a/spec/bundler/support/windows_tag_group.rb +++ b/spec/bundler/support/windows_tag_group.rb @@ -189,6 +189,7 @@ module WindowsTagGroup "spec/bundler/uri_normalizer_spec.rb", "spec/install/gems/no_build_extension_spec.rb", "spec/install/gems/no_install_plugin_spec.rb", + "spec/bundler/override_spec.rb", ], }.freeze end From bb1b229685cc5acb18f6bba71830bb9df3110fe1 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 1 May 2026 11:27:22 +0900 Subject: [PATCH 07/25] [ruby/rubygems] Add overrides attribute to Bundler::Definition Reserve a slot on Definition for the upcoming Gemfile `override` DSL. This commit only stores the data; the DSL entry point and the resolver hookup come in later commits. https://github.com/ruby/rubygems/commit/6fb2bf90fe Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/bundler/definition.rb | 4 +++- spec/bundler/bundler/definition_spec.rb | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index a620475c18cf48..2435479f9441bd 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -10,13 +10,14 @@ class << self attr_accessor :no_lock end - attr_writer :lockfile + attr_writer :lockfile, :overrides attr_reader( :dependencies, :locked_checksums, :locked_deps, :locked_gems, + :overrides, :platforms, :ruby_version, :lockfile, @@ -88,6 +89,7 @@ def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, opti @specs = nil @ruby_version = ruby_version @gemfiles = gemfiles + @overrides = [] @lockfile = lockfile @lockfile_contents = String.new diff --git a/spec/bundler/bundler/definition_spec.rb b/spec/bundler/bundler/definition_spec.rb index 8c7d5667ac6657..8c4a5a0331e238 100644 --- a/spec/bundler/bundler/definition_spec.rb +++ b/spec/bundler/bundler/definition_spec.rb @@ -3,6 +3,24 @@ require "bundler/definition" RSpec.describe Bundler::Definition do + describe "#overrides" do + before do + allow(Bundler::SharedHelpers).to receive(:find_gemfile) { bundled_app_gemfile } + end + + subject { Bundler::Definition.new(bundled_app_lock, [], Bundler::SourceList.new, {}) } + + it "defaults to an empty array" do + expect(subject.overrides).to eq([]) + end + + it "is writable" do + override = Bundler::Override.new("rails", :version, ">= 8.0") + subject.overrides = [override] + expect(subject.overrides).to eq([override]) + end + end + describe "#lock" do before do allow(Bundler::SharedHelpers).to receive(:find_gemfile) { bundled_app_gemfile } From 199af7f7cb1130b19d46ddf4208b18eee8cc9181 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 1 May 2026 11:30:50 +0900 Subject: [PATCH 08/25] [ruby/rubygems] Add `override` DSL method to Bundler::Dsl Introduce Gemfile-level `override target, field: operation, ...` that collects Bundler::Override instances and forwards them to Definition via to_definition. Validation and resolver hookup come in later commits; this commit only wires the entry point. https://github.com/ruby/rubygems/commit/e2fc49141c Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/bundler/dsl.rb | 13 +++++++++++-- spec/bundler/bundler/dsl_spec.rb | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index 6f06c4e918797d..a5df3235b14bf4 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -22,7 +22,7 @@ def self.evaluate(gemfile, lockfile, unlock) GITHUB_PULL_REQUEST_URL = %r{\Ahttps://github\.com/([A-Za-z0-9_\-\.]+/[A-Za-z0-9_\-\.]+)/pull/(\d+)\z} GITLAB_MERGE_REQUEST_URL = %r{\Ahttps://gitlab\.com/([A-Za-z0-9_\-\./]+)/-/merge_requests/(\d+)\z} - attr_reader :gemspecs, :gemfile + attr_reader :gemspecs, :gemfile, :overrides attr_accessor :dependencies def initialize @@ -40,6 +40,7 @@ def initialize @gemfile = nil @gemfiles = [] @lockfile = nil + @overrides = [] add_git_sources end @@ -184,10 +185,18 @@ def github(repo, options = {}) with_source(git_source) { yield } end + def override(target, **operations) + operations.each do |field, operation| + @overrides << Override.new(target, field, operation) + end + end + def to_definition(lockfile, unlock) check_primary_source_safety lockfile = @lockfile unless @lockfile.nil? - Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles) + definition = Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles) + definition.overrides = @overrides + definition end def group(*args, &blk) diff --git a/spec/bundler/bundler/dsl_spec.rb b/spec/bundler/bundler/dsl_spec.rb index 6ba2e728b57323..b0aa0fa05d1d17 100644 --- a/spec/bundler/bundler/dsl_spec.rb +++ b/spec/bundler/bundler/dsl_spec.rb @@ -366,4 +366,36 @@ end end end + + describe "#override" do + it "stores an Override for a gem with a version: operation" do + subject.override("rails", version: ">= 8.0") + + expect(subject.overrides.size).to eq(1) + override = subject.overrides.first + expect(override.target).to eq("rails") + expect(override.field).to eq(:version) + expect(override.operation).to eq(">= 8.0") + end + + it "accepts :ignore_upper as the operation" do + subject.override("nokogiri", version: :ignore_upper) + expect(subject.overrides.first.operation).to eq(:ignore_upper) + end + + it "accepts nil as the operation" do + subject.override("legacy", version: nil) + expect(subject.overrides.first.operation).to be_nil + end + + it "appends to overrides across multiple statements" do + subject.override("rails", version: ">= 8.0") + subject.override("nokogiri", version: :ignore_upper) + expect(subject.overrides.map(&:target)).to eq(["rails", "nokogiri"]) + end + + it "is empty by default" do + expect(subject.overrides).to eq([]) + end + end end From 7a111bebbb0f5d5a45d89dd77bc9e01ff55fe5dc Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 1 May 2026 11:50:19 +0900 Subject: [PATCH 09/25] [ruby/rubygems] Preserve != exclusions in Override :ignore_upper Switch remove_upper_bounds from a lower-bound allow-list to an upper-bound reject-list so that operators other than <, <=, and ~> are kept verbatim. The previous logic, inherited from Shopify/bundler-ignore-dependency, dropped != exclusions and could silently re-allow versions the user had explicitly pinned out (e.g. >= 1.0, != 1.5.0, < 2.0 collapsed to >= 1.0). https://github.com/ruby/rubygems/commit/7d73d9e035 Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/bundler/override.rb | 14 ++++++++------ spec/bundler/bundler/override_spec.rb | 6 ++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/bundler/override.rb b/lib/bundler/override.rb index d07dad00ea6bf8..1ca6d2fde5d4e6 100644 --- a/lib/bundler/override.rb +++ b/lib/bundler/override.rb @@ -2,7 +2,7 @@ module Bundler class Override - LOWER_BOUND_OPERATORS = [">=", ">", "="].freeze + UPPER_BOUND_OPERATORS = ["<", "<="].freeze attr_reader :target, :field, :operation @@ -30,17 +30,19 @@ def apply_to(requirement) def remove_upper_bounds(requirement) return Gem::Requirement.default if requirement.nil? || requirement.none? - lower_bounds = requirement.requirements.filter_map do |op, version| - if LOWER_BOUND_OPERATORS.include?(op) - [op, version] + preserved = requirement.requirements.filter_map do |op, version| + if UPPER_BOUND_OPERATORS.include?(op) + nil elsif op == "~>" [">=", version] + else + [op, version] end end - return Gem::Requirement.default if lower_bounds.empty? + return Gem::Requirement.default if preserved.empty? - Gem::Requirement.new(lower_bounds.map {|op, v| "#{op} #{v}" }) + Gem::Requirement.new(preserved.map {|op, v| "#{op} #{v}" }) end end end diff --git a/spec/bundler/bundler/override_spec.rb b/spec/bundler/bundler/override_spec.rb index bc813e52fc3ff9..da3b5aad87a086 100644 --- a/spec/bundler/bundler/override_spec.rb +++ b/spec/bundler/bundler/override_spec.rb @@ -35,6 +35,12 @@ expect(result).to eq(Gem::Requirement.new(">= 1.5")) end + it "preserves != exclusion constraints" do + override = described_class.new("rails", :version, :ignore_upper) + result = override.apply_to(Gem::Requirement.new(">= 1.0", "!= 1.5.0", "< 2.0")) + expect(result).to eq(Gem::Requirement.new(">= 1.0", "!= 1.5.0")) + end + it "returns the default requirement when only upper bounds remain" do override = described_class.new("rails", :version, :ignore_upper) result = override.apply_to(Gem::Requirement.new("< 2.0")) From 77c7e15a67ae4eff724158b7faf841af0f4a56c2 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 1 May 2026 12:04:49 +0900 Subject: [PATCH 10/25] [ruby/rubygems] Reject `override :all, version:` in Bundler::Dsl Version requirements are per-gem and have no meaning relative to the `:all` target. Raise ArgumentError at the DSL entry point and store nothing when this combination is given, even if other valid fields are mixed in the same call. https://github.com/ruby/rubygems/commit/aabf71f46f Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/bundler/dsl.rb | 4 ++++ spec/bundler/bundler/dsl_spec.rb | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index a5df3235b14bf4..7a4fc3909e979c 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -186,6 +186,10 @@ def github(repo, options = {}) end def override(target, **operations) + if target == :all && operations.key?(:version) + raise ArgumentError, "`override :all, version:` is not allowed; version requirements are per-gem" + end + operations.each do |field, operation| @overrides << Override.new(target, field, operation) end diff --git a/spec/bundler/bundler/dsl_spec.rb b/spec/bundler/bundler/dsl_spec.rb index b0aa0fa05d1d17..d5e5f830ce1a72 100644 --- a/spec/bundler/bundler/dsl_spec.rb +++ b/spec/bundler/bundler/dsl_spec.rb @@ -397,5 +397,24 @@ it "is empty by default" do expect(subject.overrides).to eq([]) end + + it "raises ArgumentError when target is :all and version: is given" do + expect do + subject.override(:all, version: ">= 8.0") + end.to raise_error(ArgumentError, /`override :all, version:` is not allowed/) + end + + it "rejects :all + version: even when other fields are also given" do + expect do + subject.override(:all, required_ruby_version: :ignore_upper, version: ">= 8.0") + end.to raise_error(ArgumentError, /`override :all, version:` is not allowed/) + end + + it "does not record any override when :all + version: is rejected" do + expect do + subject.override(:all, version: ">= 8.0") + end.to raise_error(ArgumentError) + expect(subject.overrides).to eq([]) + end end end From a093612797e5436dd4a8b57231c3848cff86e35b Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 1 May 2026 12:12:43 +0900 Subject: [PATCH 11/25] [ruby/rubygems] Validate override target, field, and operation types Reject anything other than :all or a String for the target, fields outside `:version`, and operations that are not a String, nil, or one of the supported Symbol values (currently only :ignore_upper). Validation runs before any Override is recorded so that a multi-field call with an invalid entry leaves the DSL state untouched. https://github.com/ruby/rubygems/commit/c3b7aeb6b9 Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/bundler/dsl.rb | 33 ++++++++++++++++++++++++++++++++ spec/bundler/bundler/dsl_spec.rb | 31 ++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index 7a4fc3909e979c..2f9d1f90547497 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -185,11 +185,21 @@ def github(repo, options = {}) with_source(git_source) { yield } end + SUPPORTED_OVERRIDE_FIELDS = [:version].freeze + SUPPORTED_OVERRIDE_SYMBOL_OPERATIONS = [:ignore_upper].freeze + def override(target, **operations) + validate_override_target!(target) + if target == :all && operations.key?(:version) raise ArgumentError, "`override :all, version:` is not allowed; version requirements are per-gem" end + operations.each do |field, operation| + validate_override_field!(field) + validate_override_operation!(operation) + end + operations.each do |field, operation| @overrides << Override.new(target, field, operation) end @@ -257,6 +267,29 @@ def check_primary_source_safety private + def validate_override_target!(target) + return if target == :all + return if target.is_a?(String) + raise ArgumentError, "override target must be :all or a gem name string, got #{target.inspect}" + end + + def validate_override_field!(field) + return if SUPPORTED_OVERRIDE_FIELDS.include?(field) + raise ArgumentError, "unsupported override field `#{field}:`; only `version:` is currently supported" + end + + def validate_override_operation!(operation) + case operation + when String, nil + # ok + when Symbol + return if SUPPORTED_OVERRIDE_SYMBOL_OPERATIONS.include?(operation) + raise ArgumentError, "unsupported override operation: #{operation.inspect}" + else + raise ArgumentError, "override operation must be a String, Symbol, or nil, got #{operation.inspect}" + end + end + def add_dependency(name, version = nil, options = {}) options["gemfile"] = @gemfile options["source"] ||= @source diff --git a/spec/bundler/bundler/dsl_spec.rb b/spec/bundler/bundler/dsl_spec.rb index d5e5f830ce1a72..86bcb3be8dbcc4 100644 --- a/spec/bundler/bundler/dsl_spec.rb +++ b/spec/bundler/bundler/dsl_spec.rb @@ -416,5 +416,36 @@ end.to raise_error(ArgumentError) expect(subject.overrides).to eq([]) end + + it "raises ArgumentError when target is neither :all nor a string" do + expect do + subject.override(:rails, version: ">= 8.0") + end.to raise_error(ArgumentError, /target must be :all or a gem name string/) + end + + it "raises ArgumentError for an unsupported field" do + expect do + subject.override("rails", required_ruby_version: :ignore_upper) + end.to raise_error(ArgumentError, /unsupported override field `required_ruby_version:`/) + end + + it "raises ArgumentError for a non-string, non-symbol, non-nil operation" do + expect do + subject.override("rails", version: 42) + end.to raise_error(ArgumentError, /override operation must be a String, Symbol, or nil/) + end + + it "raises ArgumentError for an unsupported symbol operation" do + expect do + subject.override("rails", version: :explode) + end.to raise_error(ArgumentError, /unsupported override operation/) + end + + it "rejects atomically when one field in a multi-field call is invalid" do + expect do + subject.override("rails", version: ">= 8.0", required_ruby_version: :ignore_upper) + end.to raise_error(ArgumentError, /unsupported override field/) + expect(subject.overrides).to eq([]) + end end end From 22fb622b486d17c26b9f9c04be760d31da1d96fd Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 1 May 2026 12:14:25 +0900 Subject: [PATCH 12/25] [ruby/rubygems] Reject duplicate target+field in override DSL Two override statements that target the same gem and the same field make it ambiguous which operation should win. Raise ArgumentError when the new (target, field) pair already exists in the recorded overrides, leaving the previously recorded entry untouched. https://github.com/ruby/rubygems/commit/1c90030cf9 Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/bundler/dsl.rb | 6 ++++++ spec/bundler/bundler/dsl_spec.rb | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index 2f9d1f90547497..7f14c0dca0cc7a 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -198,6 +198,7 @@ def override(target, **operations) operations.each do |field, operation| validate_override_field!(field) validate_override_operation!(operation) + validate_override_uniqueness!(target, field) end operations.each do |field, operation| @@ -290,6 +291,11 @@ def validate_override_operation!(operation) end end + def validate_override_uniqueness!(target, field) + return unless @overrides.any? {|o| o.target == target && o.field == field } + raise ArgumentError, "duplicate override for #{target.inspect} `#{field}:`" + end + def add_dependency(name, version = nil, options = {}) options["gemfile"] = @gemfile options["source"] ||= @source diff --git a/spec/bundler/bundler/dsl_spec.rb b/spec/bundler/bundler/dsl_spec.rb index 86bcb3be8dbcc4..39f745c05efe63 100644 --- a/spec/bundler/bundler/dsl_spec.rb +++ b/spec/bundler/bundler/dsl_spec.rb @@ -447,5 +447,27 @@ end.to raise_error(ArgumentError, /unsupported override field/) expect(subject.overrides).to eq([]) end + + it "raises ArgumentError when the same target and field are overridden twice" do + subject.override("rails", version: ">= 8.0") + expect do + subject.override("rails", version: :ignore_upper) + end.to raise_error(ArgumentError, /duplicate override for "rails" `version:`/) + end + + it "keeps the original override when a duplicate is rejected" do + subject.override("rails", version: ">= 8.0") + expect do + subject.override("rails", version: :ignore_upper) + end.to raise_error(ArgumentError) + expect(subject.overrides.size).to eq(1) + expect(subject.overrides.first.operation).to eq(">= 8.0") + end + + it "allows different targets with the same field" do + subject.override("rails", version: ">= 8.0") + subject.override("nokogiri", version: :ignore_upper) + expect(subject.overrides.size).to eq(2) + end end end From 464e212b961164e3730b17119a1f79b3dac7f2f9 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 1 May 2026 12:23:55 +0900 Subject: [PATCH 13/25] [ruby/rubygems] Apply override version operations in Resolver Pass the Definition's overrides through to Resolver::Base and rewrite each dependency's requirement at the entry of to_dependency_hash so both direct and transitive deps are reshaped before PubGrub sees them. The hook is the same point Shopify/bundler-ignore-dependency uses, since prepare_dependencies and the @cached_dependencies closure both funnel through to_dependency_hash. Override#apply_to handles all three operation kinds, so :ignore_upper and nil also start working from this commit; integration coverage for those paths follows in later commits. https://github.com/ruby/rubygems/commit/93b1c5b46c Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/bundler/definition.rb | 2 +- lib/bundler/resolver.rb | 12 ++++++- lib/bundler/resolver/base.rb | 3 +- spec/bundler/install/gemfile/override_spec.rb | 35 +++++++++++++++++++ spec/bundler/support/windows_tag_group.rb | 1 + 5 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 spec/bundler/install/gemfile/override_spec.rb diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 2435479f9441bd..523e6ff448103a 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -1275,7 +1275,7 @@ def unlocked_resolution_base def new_resolution_base(last_resolve:, unlock:) new_resolution_platforms = @current_platform_missing ? @new_platforms + [Bundler.local_platform] : @new_platforms - Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: unlock, prerelease: gem_version_promoter.pre?, prefer_local: @prefer_local, new_platforms: new_resolution_platforms) + Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: unlock, prerelease: gem_version_promoter.pre?, prefer_local: @prefer_local, new_platforms: new_resolution_platforms, overrides: @overrides) end def new_resolver(base) diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index 3c361d8ea51a9f..5e934e2a12e6b1 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -510,7 +510,7 @@ def requirement_to_range(requirement) end def to_dependency_hash(dependencies, packages) - dependencies.inject({}) do |deps, dep| + apply_overrides(dependencies).inject({}) do |deps, dep| package = packages[dep.name] current_req = deps[package] @@ -526,6 +526,16 @@ def to_dependency_hash(dependencies, packages) end end + def apply_overrides(dependencies) + return dependencies if @base.overrides.empty? + + dependencies.map do |dep| + override = @base.overrides.find {|o| o.target == dep.name && o.field == :version } + next dep unless override + Gem::Dependency.new(dep.name, override.apply_to(dep.requirement)) + end + end + def bundler_not_found_message(conflict_dependencies) candidate_specs = filter_matching_specs(default_bundler_source.specs.search("bundler"), conflict_dependencies) diff --git a/lib/bundler/resolver/base.rb b/lib/bundler/resolver/base.rb index 932a92ff4156ca..00bdd08303dfd1 100644 --- a/lib/bundler/resolver/base.rb +++ b/lib/bundler/resolver/base.rb @@ -5,9 +5,10 @@ module Bundler class Resolver class Base - attr_reader :packages, :requirements, :source_requirements, :locked_specs + attr_reader :packages, :requirements, :source_requirements, :locked_specs, :overrides def initialize(source_requirements, dependencies, base, platforms, options) + @overrides = options.delete(:overrides) || [] @source_requirements = source_requirements @locked_specs = options[:locked_specs] diff --git a/spec/bundler/install/gemfile/override_spec.rb b/spec/bundler/install/gemfile/override_spec.rb new file mode 100644 index 00000000000000..e37e47e9499925 --- /dev/null +++ b/spec/bundler/install/gemfile/override_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +RSpec.describe "override DSL" do + context "with a version: string operation" do + it "replaces a direct dependency requirement with the override version spec" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 0.9.1" + gem "myrack" + G + + expect(the_bundle).to include_gems "myrack 0.9.1" + end + + it "replaces a transitive dependency requirement" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 1.0.0" + gem "myrack_middleware" + G + + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0" + end + + it "replaces the requirement even when the Gemfile pins a different version" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 0.9.1" + gem "myrack", "= 1.0.0" + G + + expect(the_bundle).to include_gems "myrack 0.9.1" + end + end +end diff --git a/spec/bundler/support/windows_tag_group.rb b/spec/bundler/support/windows_tag_group.rb index 7b52b58743b61c..fb9c0811493f2c 100644 --- a/spec/bundler/support/windows_tag_group.rb +++ b/spec/bundler/support/windows_tag_group.rb @@ -190,6 +190,7 @@ module WindowsTagGroup "spec/install/gems/no_build_extension_spec.rb", "spec/install/gems/no_install_plugin_spec.rb", "spec/bundler/override_spec.rb", + "spec/install/gemfile/override_spec.rb", ], }.freeze end From 2da7365ff6f2f15b7ac72eef214c8be8d1cc5ea8 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 1 May 2026 12:37:10 +0900 Subject: [PATCH 14/25] [ruby/rubygems] Apply override at Definition level for direct dependencies The Resolver-only hook reshapes deps just before PubGrub sees them, but it does not influence the Bundler::Dependency objects fed into Definition#expanded_dependencies. As a result the Resolver::Package built from each direct dep keeps the original requirement, so its prerelease policy and (in later commits) lockfile change detection ignore the override entirely. Apply the override to direct deps at expanded_dependencies as well so that Package metadata and convergence see the effective requirement; the Resolver hook remains responsible for transitive deps fetched from gemspecs. https://github.com/ruby/rubygems/commit/6f397af4cc Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/bundler/definition.rb | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 523e6ff448103a..1308b41563396e 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -635,7 +635,20 @@ def resolver end def expanded_dependencies - dependencies_with_bundler + metadata_dependencies + apply_overrides_to(dependencies_with_bundler) + metadata_dependencies + end + + def apply_overrides_to(deps) + return deps if @overrides.empty? + deps.map {|dep| apply_override_to(dep) } + end + + def apply_override_to(dep) + override = @overrides.find {|o| o.target == dep.name && o.field == :version } + return dep unless override + new_dep = dep.dup + new_dep.instance_variable_set(:@requirement, override.apply_to(dep.requirement)) + new_dep end def dependencies_with_bundler From 41a558ae768d08fba9c88a6c51ecc11cce2fc0b3 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 1 May 2026 12:43:58 +0900 Subject: [PATCH 15/25] [ruby/rubygems] Mark overridden gems as changed against the existing lockfile converge_dependencies compared the original Gemfile dependency against the locked spec, so adding `override "foo", version: "= X"` to a previously installed bundle did not register as a change. additional_base_requirements_to_prevent_downgrades then kept forwarding the locked version as a >= base requirement, which intersected the override and made it a no-op. Pass overrides into Definition.new positionally so they are available before the constructor calls converge_dependencies, and compare each direct dep's effective requirement (after override) to the locked spec. https://github.com/ruby/rubygems/commit/248e1ba385 Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/bundler/definition.rb | 6 +++--- lib/bundler/dsl.rb | 4 +--- spec/bundler/install/gemfile/override_spec.rb | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 1308b41563396e..c2eab0efc6fb16 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -59,7 +59,7 @@ def self.build(gemfile, lockfile, unlock) # to be updated or true if all gems should be updated # @param ruby_version [Bundler::RubyVersion, nil] Requested Ruby Version # @param optional_groups [Array(String)] A list of optional groups - def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, optional_groups = [], gemfiles = []) + def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, optional_groups = [], gemfiles = [], overrides = []) unlock ||= {} if unlock == true @@ -89,7 +89,7 @@ def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, opti @specs = nil @ruby_version = ruby_version @gemfiles = gemfiles - @overrides = [] + @overrides = overrides @lockfile = lockfile @lockfile_contents = String.new @@ -1044,7 +1044,7 @@ def converge_dependencies @locked_specs.delete(locked_specs.select {|s| s.source != dep.source }) end - unless dep.matches_spec?(locked_specs.first) + unless apply_override_to(dep).matches_spec?(locked_specs.first) @gems_to_unlock << name dep_changed = true end diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index 7f14c0dca0cc7a..c7a7d855eef179 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -209,9 +209,7 @@ def override(target, **operations) def to_definition(lockfile, unlock) check_primary_source_safety lockfile = @lockfile unless @lockfile.nil? - definition = Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles) - definition.overrides = @overrides - definition + Definition.new(lockfile, @dependencies, @sources, unlock, @ruby_version, @optional_groups, @gemfiles, @overrides) end def group(*args, &blk) diff --git a/spec/bundler/install/gemfile/override_spec.rb b/spec/bundler/install/gemfile/override_spec.rb index e37e47e9499925..d3fdace4aef654 100644 --- a/spec/bundler/install/gemfile/override_spec.rb +++ b/spec/bundler/install/gemfile/override_spec.rb @@ -31,5 +31,24 @@ expect(the_bundle).to include_gems "myrack 0.9.1" end + + it "applies the override against an existing lockfile" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + + gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 0.9.1" + gem "myrack" + G + + bundle :install + + expect(the_bundle).to include_gems "myrack 0.9.1" + end end end From d763aec78246856d26b58e5cd88d40be2e9c2e96 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 1 May 2026 12:45:02 +0900 Subject: [PATCH 16/25] [ruby/rubygems] Cover prerelease pin via override When the Gemfile dependency does not request a prerelease, Resolver::Package's prerelease policy normally filters them out. Because Definition#expanded_dependencies now feeds the effective (override-applied) dep into Resolver::Base, an override that pins an exact prerelease propagates into the package's prerelease decision and the prerelease becomes selectable. Lock that contract in with an integration test on a has_prerelease 1.0 / 1.1.pre fixture. https://github.com/ruby/rubygems/commit/a40b224354 Co-Authored-By: Claude Opus 4.7 (1M context) --- spec/bundler/install/gemfile/override_spec.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec/bundler/install/gemfile/override_spec.rb b/spec/bundler/install/gemfile/override_spec.rb index d3fdace4aef654..4f2eb8544d7548 100644 --- a/spec/bundler/install/gemfile/override_spec.rb +++ b/spec/bundler/install/gemfile/override_spec.rb @@ -50,5 +50,20 @@ expect(the_bundle).to include_gems "myrack 0.9.1" end + + it "pins a prerelease version that the Gemfile dependency would otherwise filter out" do + build_repo2 do + build_gem "has_prerelease", "1.0" + build_gem "has_prerelease", "1.1.pre" + end + + install_gemfile <<-G + source "https://gem.repo2" + override "has_prerelease", version: "= 1.1.pre" + gem "has_prerelease" + G + + expect(the_bundle).to include_gems "has_prerelease 1.1.pre" + end end end From b5f31bbd19a1a3130d441479d5c12700b285f0b0 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 1 May 2026 12:46:17 +0900 Subject: [PATCH 17/25] [ruby/rubygems] Add :ignore_upper integration coverage Verify end-to-end that override(..., version: :ignore_upper) drops < / <= bounds and folds ~> into >= so a Gemfile-pinned 0.9.1 ceiling no longer prevents myrack 1.0.0 from being chosen. https://github.com/ruby/rubygems/commit/95224cff50 Co-Authored-By: Claude Opus 4.7 (1M context) --- spec/bundler/install/gemfile/override_spec.rb | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/spec/bundler/install/gemfile/override_spec.rb b/spec/bundler/install/gemfile/override_spec.rb index 4f2eb8544d7548..0b5f868d42a00b 100644 --- a/spec/bundler/install/gemfile/override_spec.rb +++ b/spec/bundler/install/gemfile/override_spec.rb @@ -66,4 +66,26 @@ expect(the_bundle).to include_gems "has_prerelease 1.1.pre" end end + + context "with a version: :ignore_upper operation" do + it "strips a < upper bound on a direct dependency" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: :ignore_upper + gem "myrack", "< 1.0" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "folds ~> into >= so newer versions become reachable" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: :ignore_upper + gem "myrack", "~> 0.9.1" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + end end From 1fa48314b10de6d2c9f031daea8fc82e6216d3ca Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 1 May 2026 12:47:07 +0900 Subject: [PATCH 18/25] [ruby/rubygems] Add version: nil integration coverage Verify end-to-end that override(..., version: nil) collapses the requirement to Gem::Requirement.default for both direct deps (a Gemfile pin to 0.9.1 is removed) and transitive deps (a myrack_middleware-imposed = 0.9.1 floor is removed). https://github.com/ruby/rubygems/commit/7df463f5c7 Co-Authored-By: Claude Opus 4.7 (1M context) --- spec/bundler/install/gemfile/override_spec.rb | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/spec/bundler/install/gemfile/override_spec.rb b/spec/bundler/install/gemfile/override_spec.rb index 0b5f868d42a00b..8a00186d60a002 100644 --- a/spec/bundler/install/gemfile/override_spec.rb +++ b/spec/bundler/install/gemfile/override_spec.rb @@ -88,4 +88,26 @@ expect(the_bundle).to include_gems "myrack 1.0.0" end end + + context "with a version: nil operation" do + it "drops a direct dependency's pin entirely" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: nil + gem "myrack", "= 0.9.1" + G + + expect(the_bundle).to include_gems "myrack 1.0.0" + end + + it "drops a transitive dependency's pin entirely" do + install_gemfile <<-G + source "https://gem.repo1" + override "myrack", version: nil + gem "myrack_middleware" + G + + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0" + end + end end From ddabda72215c93af76f9fcc3c79660072833e683 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 1 May 2026 12:51:32 +0900 Subject: [PATCH 19/25] [ruby/rubygems] Document the override DSL in gemfile.5.ronn Add an OVERRIDE section between GEMSPEC and SOURCE PRIORITY that covers the syntax, the three operations (version spec string, :ignore_upper, nil), and the lockfile-vs-resolution boundary. https://github.com/ruby/rubygems/commit/275cbcaef3 Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/bundler/man/bundle-add.1 | 2 +- lib/bundler/man/bundle-binstubs.1 | 2 +- lib/bundler/man/bundle-cache.1 | 2 +- lib/bundler/man/bundle-check.1 | 2 +- lib/bundler/man/bundle-clean.1 | 2 +- lib/bundler/man/bundle-config.1 | 2 +- lib/bundler/man/bundle-console.1 | 2 +- lib/bundler/man/bundle-doctor.1 | 2 +- lib/bundler/man/bundle-env.1 | 2 +- lib/bundler/man/bundle-exec.1 | 2 +- lib/bundler/man/bundle-fund.1 | 2 +- lib/bundler/man/bundle-gem.1 | 2 +- lib/bundler/man/bundle-help.1 | 2 +- lib/bundler/man/bundle-info.1 | 2 +- lib/bundler/man/bundle-init.1 | 2 +- lib/bundler/man/bundle-install.1 | 2 +- lib/bundler/man/bundle-issue.1 | 2 +- lib/bundler/man/bundle-licenses.1 | 2 +- lib/bundler/man/bundle-list.1 | 2 +- lib/bundler/man/bundle-lock.1 | 2 +- lib/bundler/man/bundle-open.1 | 2 +- lib/bundler/man/bundle-outdated.1 | 2 +- lib/bundler/man/bundle-platform.1 | 2 +- lib/bundler/man/bundle-plugin.1 | 2 +- lib/bundler/man/bundle-pristine.1 | 2 +- lib/bundler/man/bundle-remove.1 | 2 +- lib/bundler/man/bundle-show.1 | 2 +- lib/bundler/man/bundle-update.1 | 2 +- lib/bundler/man/bundle-version.1 | 2 +- lib/bundler/man/bundle.1 | 2 +- lib/bundler/man/gemfile.5 | 38 +++++++++++++++++++++++++++++- lib/bundler/man/gemfile.5.ronn | 39 +++++++++++++++++++++++++++++++ 32 files changed, 106 insertions(+), 31 deletions(-) diff --git a/lib/bundler/man/bundle-add.1 b/lib/bundler/man/bundle-add.1 index f49d8e568c9bed..031eef686c9427 100644 --- a/lib/bundler/man/bundle-add.1 +++ b/lib/bundler/man/bundle-add.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-ADD" "1" "April 2026" "" +.TH "BUNDLE\-ADD" "1" "May 2026" "" .SH "NAME" \fBbundle\-add\fR \- Add gem to the Gemfile and run bundle install .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-binstubs.1 b/lib/bundler/man/bundle-binstubs.1 index 9dbd4f1d3a1f90..246daeae53edb1 100644 --- a/lib/bundler/man/bundle-binstubs.1 +++ b/lib/bundler/man/bundle-binstubs.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-BINSTUBS" "1" "April 2026" "" +.TH "BUNDLE\-BINSTUBS" "1" "May 2026" "" .SH "NAME" \fBbundle\-binstubs\fR \- Install the binstubs of the listed gems .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-cache.1 b/lib/bundler/man/bundle-cache.1 index e2052ab0ac1606..38ea0479612404 100644 --- a/lib/bundler/man/bundle-cache.1 +++ b/lib/bundler/man/bundle-cache.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CACHE" "1" "April 2026" "" +.TH "BUNDLE\-CACHE" "1" "May 2026" "" .SH "NAME" \fBbundle\-cache\fR \- Package your needed \fB\.gem\fR files into your application .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-check.1 b/lib/bundler/man/bundle-check.1 index 825a2889d57a88..6cd474d90ab795 100644 --- a/lib/bundler/man/bundle-check.1 +++ b/lib/bundler/man/bundle-check.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CHECK" "1" "April 2026" "" +.TH "BUNDLE\-CHECK" "1" "May 2026" "" .SH "NAME" \fBbundle\-check\fR \- Verifies if dependencies are satisfied by installed gems .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-clean.1 b/lib/bundler/man/bundle-clean.1 index 0eae33d08d1926..eb90636c17170b 100644 --- a/lib/bundler/man/bundle-clean.1 +++ b/lib/bundler/man/bundle-clean.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CLEAN" "1" "April 2026" "" +.TH "BUNDLE\-CLEAN" "1" "May 2026" "" .SH "NAME" \fBbundle\-clean\fR \- Cleans up unused gems in your bundler directory .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1 index 8835ae75bd9e92..61487ca55e7921 100644 --- a/lib/bundler/man/bundle-config.1 +++ b/lib/bundler/man/bundle-config.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CONFIG" "1" "April 2026" "" +.TH "BUNDLE\-CONFIG" "1" "May 2026" "" .SH "NAME" \fBbundle\-config\fR \- Set bundler configuration options .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-console.1 b/lib/bundler/man/bundle-console.1 index c86b90e3bd23f1..5d3f65365faaef 100644 --- a/lib/bundler/man/bundle-console.1 +++ b/lib/bundler/man/bundle-console.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-CONSOLE" "1" "April 2026" "" +.TH "BUNDLE\-CONSOLE" "1" "May 2026" "" .SH "NAME" \fBbundle\-console\fR \- Open an IRB session with the bundle pre\-loaded .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-doctor.1 b/lib/bundler/man/bundle-doctor.1 index fe9a8a35b9afa8..4c59871b66ad55 100644 --- a/lib/bundler/man/bundle-doctor.1 +++ b/lib/bundler/man/bundle-doctor.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-DOCTOR" "1" "April 2026" "" +.TH "BUNDLE\-DOCTOR" "1" "May 2026" "" .SH "NAME" \fBbundle\-doctor\fR \- Checks the bundle for common problems .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-env.1 b/lib/bundler/man/bundle-env.1 index 29c4ac2a8e21a5..25fcb648917f76 100644 --- a/lib/bundler/man/bundle-env.1 +++ b/lib/bundler/man/bundle-env.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-ENV" "1" "April 2026" "" +.TH "BUNDLE\-ENV" "1" "May 2026" "" .SH "NAME" \fBbundle\-env\fR \- Print information about the environment Bundler is running under .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-exec.1 b/lib/bundler/man/bundle-exec.1 index fec7bee39c338c..c3a6a09d578ae6 100644 --- a/lib/bundler/man/bundle-exec.1 +++ b/lib/bundler/man/bundle-exec.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-EXEC" "1" "April 2026" "" +.TH "BUNDLE\-EXEC" "1" "May 2026" "" .SH "NAME" \fBbundle\-exec\fR \- Execute a command in the context of the bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-fund.1 b/lib/bundler/man/bundle-fund.1 index 2eb07a6c8d8cad..caee1f81dd64a0 100644 --- a/lib/bundler/man/bundle-fund.1 +++ b/lib/bundler/man/bundle-fund.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-FUND" "1" "April 2026" "" +.TH "BUNDLE\-FUND" "1" "May 2026" "" .SH "NAME" \fBbundle\-fund\fR \- Lists information about gems seeking funding assistance .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-gem.1 b/lib/bundler/man/bundle-gem.1 index bdb84faebdd0a2..87d756824698c9 100644 --- a/lib/bundler/man/bundle-gem.1 +++ b/lib/bundler/man/bundle-gem.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-GEM" "1" "April 2026" "" +.TH "BUNDLE\-GEM" "1" "May 2026" "" .SH "NAME" \fBbundle\-gem\fR \- Generate a project skeleton for creating a rubygem .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-help.1 b/lib/bundler/man/bundle-help.1 index 6e6ad14624d739..3bcfd047e5464b 100644 --- a/lib/bundler/man/bundle-help.1 +++ b/lib/bundler/man/bundle-help.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-HELP" "1" "April 2026" "" +.TH "BUNDLE\-HELP" "1" "May 2026" "" .SH "NAME" \fBbundle\-help\fR \- Displays detailed help for each subcommand .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-info.1 b/lib/bundler/man/bundle-info.1 index b18b70309c505d..49c2295f8c981f 100644 --- a/lib/bundler/man/bundle-info.1 +++ b/lib/bundler/man/bundle-info.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-INFO" "1" "April 2026" "" +.TH "BUNDLE\-INFO" "1" "May 2026" "" .SH "NAME" \fBbundle\-info\fR \- Show information for the given gem in your bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-init.1 b/lib/bundler/man/bundle-init.1 index 5ea1c3b4783733..63e2376c3fd3fb 100644 --- a/lib/bundler/man/bundle-init.1 +++ b/lib/bundler/man/bundle-init.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-INIT" "1" "April 2026" "" +.TH "BUNDLE\-INIT" "1" "May 2026" "" .SH "NAME" \fBbundle\-init\fR \- Generates a Gemfile into the current working directory .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-install.1 b/lib/bundler/man/bundle-install.1 index c2f9bfeea1f79b..a764d031ed12c3 100644 --- a/lib/bundler/man/bundle-install.1 +++ b/lib/bundler/man/bundle-install.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-INSTALL" "1" "April 2026" "" +.TH "BUNDLE\-INSTALL" "1" "May 2026" "" .SH "NAME" \fBbundle\-install\fR \- Install the dependencies specified in your Gemfile .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-issue.1 b/lib/bundler/man/bundle-issue.1 index e99cf67638f392..3af277ef867a95 100644 --- a/lib/bundler/man/bundle-issue.1 +++ b/lib/bundler/man/bundle-issue.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-ISSUE" "1" "April 2026" "" +.TH "BUNDLE\-ISSUE" "1" "May 2026" "" .SH "NAME" \fBbundle\-issue\fR \- Get help reporting Bundler issues .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-licenses.1 b/lib/bundler/man/bundle-licenses.1 index eb5f7203ec1d76..ab5996d2be7a23 100644 --- a/lib/bundler/man/bundle-licenses.1 +++ b/lib/bundler/man/bundle-licenses.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-LICENSES" "1" "April 2026" "" +.TH "BUNDLE\-LICENSES" "1" "May 2026" "" .SH "NAME" \fBbundle\-licenses\fR \- Print the license of all gems in the bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-list.1 b/lib/bundler/man/bundle-list.1 index 69276822d2cd00..e759e0d4493b3c 100644 --- a/lib/bundler/man/bundle-list.1 +++ b/lib/bundler/man/bundle-list.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-LIST" "1" "April 2026" "" +.TH "BUNDLE\-LIST" "1" "May 2026" "" .SH "NAME" \fBbundle\-list\fR \- List all the gems in the bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-lock.1 b/lib/bundler/man/bundle-lock.1 index ba1915af2e1597..396c8ff6ca9766 100644 --- a/lib/bundler/man/bundle-lock.1 +++ b/lib/bundler/man/bundle-lock.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-LOCK" "1" "April 2026" "" +.TH "BUNDLE\-LOCK" "1" "May 2026" "" .SH "NAME" \fBbundle\-lock\fR \- Creates / Updates a lockfile without installing .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-open.1 b/lib/bundler/man/bundle-open.1 index 99166e8580f4cf..2aab59f14b9b16 100644 --- a/lib/bundler/man/bundle-open.1 +++ b/lib/bundler/man/bundle-open.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-OPEN" "1" "April 2026" "" +.TH "BUNDLE\-OPEN" "1" "May 2026" "" .SH "NAME" \fBbundle\-open\fR \- Opens the source directory for a gem in your bundle .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-outdated.1 b/lib/bundler/man/bundle-outdated.1 index 87725b9029e3da..b739234d8daf07 100644 --- a/lib/bundler/man/bundle-outdated.1 +++ b/lib/bundler/man/bundle-outdated.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-OUTDATED" "1" "April 2026" "" +.TH "BUNDLE\-OUTDATED" "1" "May 2026" "" .SH "NAME" \fBbundle\-outdated\fR \- List installed gems with newer versions available .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-platform.1 b/lib/bundler/man/bundle-platform.1 index 486e2d4406bf6d..39b71112635311 100644 --- a/lib/bundler/man/bundle-platform.1 +++ b/lib/bundler/man/bundle-platform.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-PLATFORM" "1" "April 2026" "" +.TH "BUNDLE\-PLATFORM" "1" "May 2026" "" .SH "NAME" \fBbundle\-platform\fR \- Displays platform compatibility information .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-plugin.1 b/lib/bundler/man/bundle-plugin.1 index 1c3feead7630ee..d182c7789ba12f 100644 --- a/lib/bundler/man/bundle-plugin.1 +++ b/lib/bundler/man/bundle-plugin.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-PLUGIN" "1" "April 2026" "" +.TH "BUNDLE\-PLUGIN" "1" "May 2026" "" .SH "NAME" \fBbundle\-plugin\fR \- Manage Bundler plugins .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-pristine.1 b/lib/bundler/man/bundle-pristine.1 index cbfc51399a7029..f6cc06657161dc 100644 --- a/lib/bundler/man/bundle-pristine.1 +++ b/lib/bundler/man/bundle-pristine.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-PRISTINE" "1" "April 2026" "" +.TH "BUNDLE\-PRISTINE" "1" "May 2026" "" .SH "NAME" \fBbundle\-pristine\fR \- Restores installed gems to their pristine condition .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-remove.1 b/lib/bundler/man/bundle-remove.1 index f8981f9fcfcbe1..2ca40e74db4fb2 100644 --- a/lib/bundler/man/bundle-remove.1 +++ b/lib/bundler/man/bundle-remove.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-REMOVE" "1" "April 2026" "" +.TH "BUNDLE\-REMOVE" "1" "May 2026" "" .SH "NAME" \fBbundle\-remove\fR \- Removes gems from the Gemfile .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-show.1 b/lib/bundler/man/bundle-show.1 index aaf146fa271b8d..a2142694b8d317 100644 --- a/lib/bundler/man/bundle-show.1 +++ b/lib/bundler/man/bundle-show.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-SHOW" "1" "April 2026" "" +.TH "BUNDLE\-SHOW" "1" "May 2026" "" .SH "NAME" \fBbundle\-show\fR \- Shows all the gems in your bundle, or the path to a gem .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-update.1 b/lib/bundler/man/bundle-update.1 index e5f18f2a1e2674..6a749644e31a1d 100644 --- a/lib/bundler/man/bundle-update.1 +++ b/lib/bundler/man/bundle-update.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-UPDATE" "1" "April 2026" "" +.TH "BUNDLE\-UPDATE" "1" "May 2026" "" .SH "NAME" \fBbundle\-update\fR \- Update your gems to the latest available versions .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle-version.1 b/lib/bundler/man/bundle-version.1 index 24b5dcef45d7a5..751a408312a900 100644 --- a/lib/bundler/man/bundle-version.1 +++ b/lib/bundler/man/bundle-version.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE\-VERSION" "1" "April 2026" "" +.TH "BUNDLE\-VERSION" "1" "May 2026" "" .SH "NAME" \fBbundle\-version\fR \- Prints Bundler version information .SH "SYNOPSIS" diff --git a/lib/bundler/man/bundle.1 b/lib/bundler/man/bundle.1 index 492de63295a132..167815631a2b64 100644 --- a/lib/bundler/man/bundle.1 +++ b/lib/bundler/man/bundle.1 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "BUNDLE" "1" "April 2026" "" +.TH "BUNDLE" "1" "May 2026" "" .SH "NAME" \fBbundle\fR \- Ruby Dependency Management .SH "SYNOPSIS" diff --git a/lib/bundler/man/gemfile.5 b/lib/bundler/man/gemfile.5 index db04250b8b8c7e..0874bb5a4a7baf 100644 --- a/lib/bundler/man/gemfile.5 +++ b/lib/bundler/man/gemfile.5 @@ -1,6 +1,6 @@ .\" generated with Ronn-NG/v0.10.1 .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 -.TH "GEMFILE" "5" "April 2026" "" +.TH "GEMFILE" "5" "May 2026" "" .SH "NAME" \fBGemfile\fR \- A format for describing gem dependencies for Ruby programs .SH "SYNOPSIS" @@ -460,6 +460,42 @@ The \fBgemspec\fR method adds any runtime dependencies as gem requirements in th The \fBgemspec\fR method supports optional \fB:path\fR, \fB:glob\fR, \fB:name\fR, and \fB:development_group\fR options, which control where bundler looks for the \fB\.gemspec\fR, the glob it uses to look for the gemspec (defaults to: \fB{,*,*/*}\.gemspec\fR), what named \fB\.gemspec\fR it uses (if more than one is present), and which group development dependencies are included in\. .P When a \fBgemspec\fR dependency encounters version conflicts during resolution, the local version under development will always be selected \-\- even if there are remote versions that better match other requirements for the \fBgemspec\fR gem\. +.SH "OVERRIDE" +The \fBoverride\fR directive rewrites the version requirement on another gem before resolution runs\. It targets the common case where an upstream gem's published metadata is too narrow on the current project's machine \-\- a stale upper bound, an unwanted floor, or a transitive pin that has to be lifted\. +.IP "" 4 +.nf +override , : +.fi +.IP "" 0 +.P +\fB\fR is a gem name string\. \fB\fR is \fBversion:\fR\. \fB\fR is one of: +.IP "\(bu" 4 +a version requirement string (e\.g\. \fB">= 8\.0"\fR), which \fBreplaces\fR the target's version requirement absolutely\. The original requirement, both direct and transitive, is discarded in favour of the override\. +.IP "\(bu" 4 +\fB:ignore_upper\fR, which removes upper\-bound operators (\fB<\fR and \fB<=\fR) from the existing requirement and folds \fB~>\fR into its lower bound (\fB~> 1\.5\fR becomes \fB>= 1\.5\fR)\. Other operators, including \fB!=\fR, are preserved\. +.IP "\(bu" 4 +\fBnil\fR, which collapses the requirement to \fB>= 0\fR (no constraint at all)\. +.IP "" 0 +.P +Multiple \fBoverride\fR calls for distinct targets are allowed; declaring the same \fBtarget\fR and \fBfield\fR twice is an error\. +.IP "" 4 +.nf +source "https://rubygems\.org" + +# Force every reference to "rails" \-\- direct or transitive \-\- to >= 8\.0\. +override "rails", version: ">= 8\.0" + +# Strip the upper bound on nokogiri\. +override "nokogiri", version: :ignore_upper + +# Drop the version pin on legacy entirely\. +override "legacy", version: nil + +gem "rails", "~> 7\.0" +.fi +.IP "" 0 +.P +The override only affects resolution; \fBGemfile\.lock\fR continues to reflect the resolved versions, not the rewritten requirements\. .SH "SOURCE PRIORITY" When attempting to locate a gem to satisfy a gem requirement, bundler uses the following priority order: .IP "1." 4 diff --git a/lib/bundler/man/gemfile.5.ronn b/lib/bundler/man/gemfile.5.ronn index 18d7bb826e4439..1779fc0a015209 100644 --- a/lib/bundler/man/gemfile.5.ronn +++ b/lib/bundler/man/gemfile.5.ronn @@ -541,6 +541,45 @@ When a `gemspec` dependency encounters version conflicts during resolution, the local version under development will always be selected -- even if there are remote versions that better match other requirements for the `gemspec` gem. +## OVERRIDE + +The `override` directive rewrites the version requirement on another gem +before resolution runs. It targets the common case where an upstream gem's +published metadata is too narrow on the current project's machine -- a stale +upper bound, an unwanted floor, or a transitive pin that has to be lifted. + + override , : + +`` is a gem name string. `` is `version:`. `` is +one of: + + * a version requirement string (e.g. `">= 8.0"`), which **replaces** the + target's version requirement absolutely. The original requirement, both + direct and transitive, is discarded in favour of the override. + * `:ignore_upper`, which removes upper-bound operators (`<` and `<=`) from + the existing requirement and folds `~>` into its lower bound (`~> 1.5` + becomes `>= 1.5`). Other operators, including `!=`, are preserved. + * `nil`, which collapses the requirement to `>= 0` (no constraint at all). + +Multiple `override` calls for distinct targets are allowed; declaring the +same `target` and `field` twice is an error. + + source "https://rubygems.org" + + # Force every reference to "rails" -- direct or transitive -- to >= 8.0. + override "rails", version: ">= 8.0" + + # Strip the upper bound on nokogiri. + override "nokogiri", version: :ignore_upper + + # Drop the version pin on legacy entirely. + override "legacy", version: nil + + gem "rails", "~> 7.0" + +The override only affects resolution; `Gemfile.lock` continues to reflect +the resolved versions, not the rewritten requirements. + ## SOURCE PRIORITY When attempting to locate a gem to satisfy a gem requirement, From 9cc8943d8ddd7b4aae9b41cadb3f753cecaef16b Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 1 May 2026 13:38:49 +0900 Subject: [PATCH 20/25] [ruby/rubygems] Mark transitive-only overrides as changed against the lockfile converge_dependencies only iterates @dependencies (Gemfile-declared direct deps), so an override that targets a gem present only as a transitive dependency never registered as a change. With an existing lockfile, @dependency_changes stayed false, the resolver was skipped, and the override was a silent no-op. After the direct-dep loop, inspect @overrides for any String target that is locked but not a direct dep and force it onto @gems_to_unlock / @changed_dependencies so resolution runs and the Resolver-side override hook applies. https://github.com/ruby/rubygems/commit/a4f8f386f2 Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/bundler/definition.rb | 16 ++++++++++++++++ spec/bundler/install/gemfile/override_spec.rb | 19 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index c2eab0efc6fb16..a64b67cb44d020 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -1054,9 +1054,25 @@ def converge_dependencies @changed_dependencies << name if dep_changed end + converge_overrides_outside_dependencies + @changed_dependencies.any? end + def converge_overrides_outside_dependencies + @overrides.each do |override| + next unless override.target.is_a?(String) + + name = override.target + next if @changed_dependencies.include?(name) + next if @dependencies.any? {|d| d.name == name } + next if @originally_locked_specs[name].empty? + + @gems_to_unlock << name + @changed_dependencies << name + end + end + # Remove elements from the locked specs that are expired. This will most # commonly happen if the Gemfile has changed since the lockfile was last # generated diff --git a/spec/bundler/install/gemfile/override_spec.rb b/spec/bundler/install/gemfile/override_spec.rb index 8a00186d60a002..7a7f8078a81753 100644 --- a/spec/bundler/install/gemfile/override_spec.rb +++ b/spec/bundler/install/gemfile/override_spec.rb @@ -109,5 +109,24 @@ expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0" end + + it "applies a transitive-only override against an existing lockfile" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack_middleware" + G + + expect(the_bundle).to include_gems "myrack 0.9.1", "myrack_middleware 1.0" + + gemfile <<-G + source "https://gem.repo1" + override "myrack", version: "= 1.0.0" + gem "myrack_middleware" + G + + bundle :install + + expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0" + end end end From e8a09f9ad789d1c0948e048eb7c66288feb1a59d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Wed, 6 May 2026 12:53:44 +0200 Subject: [PATCH 21/25] Remove unused variable warning in imemo_fields_free --- imemo.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/imemo.c b/imemo.c index 6027884f9bd631..a6550a11a79b53 100644 --- a/imemo.c +++ b/imemo.c @@ -560,10 +560,7 @@ static inline void imemo_fields_free(struct rb_fields *fields) { if (FL_TEST_RAW((VALUE)fields, OBJ_FIELD_HEAP)) { -#if RUBY_DEBUG - shape_id_t shape_id = RBASIC_SHAPE_ID((VALUE)fields); - RUBY_ASSERT(rb_shape_complex_p(shape_id)); -#endif + RUBY_ASSERT(rb_shape_complex_p(RBASIC_SHAPE_ID((VALUE)fields))); st_free_table(fields->as.complex.table); } } From 4ed7fa5e4c0177d9e4be96f761d7b2b2d8974eda Mon Sep 17 00:00:00 2001 From: "Daisuke Fujimura (fd0)" Date: Mon, 4 May 2026 23:03:03 +0900 Subject: [PATCH 22/25] fix: honor --disable-multiarch in configure - AC_ARG_ENABLE's action-if-given ran unconditionally for both --enable-multiarch and --disable-multiarch, setting multiarch="" in both cases - ${multiarch+set} treats an empty-string variable as set, so --disable-multiarch was silently ignored and multiarch stayed enabled - Use AS_CASE([$enableval], ...) to unset multiarch when "no" is given --- configure.ac | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 83d76c1784346c..caf3475a38d0b9 100644 --- a/configure.ac +++ b/configure.ac @@ -3580,7 +3580,8 @@ AS_CASE(["$target_os"], AC_ARG_ENABLE(multiarch, AS_HELP_STRING([--enable-multiarch], [enable multiarch compatible directories]), - [multiarch=], [unset multiarch]) + [AS_CASE([$enableval], [no], [unset multiarch], [multiarch=])], + [unset multiarch]) AS_IF([test ${multiarch+set}], [ AC_DEFINE(ENABLE_MULTIARCH) ]) From 18ae8d6deda99d9f5bf5d372c27a72f95d1f7948 Mon Sep 17 00:00:00 2001 From: Mikhail Dmitrichenko Date: Tue, 5 May 2026 17:19:25 +0300 Subject: [PATCH 23/25] dir.c: fix dirent leak on glob_opendir realloc failure In glob_opendir(), each directory entry is copied before the entries array is grown. If growing ent->sort.entries fails, the function jumps to the nomem label before the copied entry is stored in the array. glob_dir_finish() only frees entries already recorded in ent->sort.entries, so the current rdp is leaked on that error path. Free rdp before jumping to nomem. Signed-off-by: Mikhail Dmitrichenko --- dir.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dir.c b/dir.c index d81ae28ee9e1b3..9f2d36b633f09f 100644 --- a/dir.c +++ b/dir.c @@ -2803,8 +2803,10 @@ glob_opendir(ruby_glob_entries_t *ent, DIR *dirp, int flags, rb_encoding *enc) } if (count >= capacity) { capacity += 256; - if (!(newp = GLOB_REALLOC_N(ent->sort.entries, capacity))) + if (!(newp = GLOB_REALLOC_N(ent->sort.entries, capacity))) { + GLOB_FREE(rdp); goto nomem; + } ent->sort.entries = newp; } ent->sort.entries[count++] = rdp; From 58ae5ea7ee00c2f31a678574a7ccab28716cb4d0 Mon Sep 17 00:00:00 2001 From: Sampo Kuokkanen Date: Thu, 7 May 2026 19:00:48 +0900 Subject: [PATCH 24/25] Split test_freeze_inside_sort! and reduce comparator count The first sub-test froze on the 6th comparator call. CRuby's insertion sort makes 10 comparisons reversing [1,2,3,4,5], but TimSort detects the descending run in 4 and never reaches 6 and the freeze line silently does nothing. Separately, three independent paths (block + numeric, block + non-numeric, no-block + non-numeric) were bundled into one method even though the test had independent setup. Split into: test_freeze_inside_sort_bang test_freeze_inside_sort_bang_non_numeric_block test_freeze_inside_sort_bang_non_numeric_no_block --- test/ruby/test_array.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/ruby/test_array.rb b/test/ruby/test_array.rb index 04e15b6d87db72..cad9bf5cc89a6d 100644 --- a/test/ruby/test_array.rb +++ b/test/ruby/test_array.rb @@ -1846,19 +1846,21 @@ def test_sort! assert_equal([1, 2, 3, 4], a) end - def test_freeze_inside_sort! + def test_freeze_inside_sort_bang array = [1, 2, 3, 4, 5] frozen_array = nil assert_raise(FrozenError) do count = 0 array.sort! do |a, b| - array.freeze if (count += 1) == 6 + array.freeze if (count += 1) == 3 frozen_array ||= array.map.to_a if array.frozen? b <=> a end end assert_equal(frozen_array, array) + end + def test_freeze_inside_sort_bang_non_numeric_block object = Object.new array = [1, 2, 3, 4, 5] object.define_singleton_method(:>){|_| array.freeze; true} @@ -1867,7 +1869,9 @@ def test_freeze_inside_sort! object end end + end + def test_freeze_inside_sort_bang_non_numeric_no_block object = Object.new array = [object, object] object.define_singleton_method(:>){|_| array.freeze; true} From b6e4fa71d514796ee826b1257bfd7b2a177f5f09 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 7 May 2026 20:32:26 +0900 Subject: [PATCH 25/25] [ruby/rubygems] Use Pathname#absolute? `Pathname::SEPARATOR_PAT` should be private, but was not set to private just due to a typo. https://github.com/ruby/rubygems/commit/67ce6df4c9 --- lib/bundler/source/path.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/bundler/source/path.rb b/lib/bundler/source/path.rb index 82e782ba257a6a..366a23aea725d7 100644 --- a/lib/bundler/source/path.rb +++ b/lib/bundler/source/path.rb @@ -220,10 +220,11 @@ def generate_bin(spec, options = {}) # Some gem authors put absolute paths in their gemspec # and we have to save them from themselves spec.files = spec.files.filter_map do |path| - next path unless /\A#{Pathname::SEPARATOR_PAT}/o.match?(path) + pathname = Pathname.new(path) + next path unless pathname.absolute? next if File.directory?(path) begin - Pathname.new(path).relative_path_from(gem_dir).to_s + pathname.relative_path_from(gem_dir).to_s rescue ArgumentError path end