From e0075945bc449846b863df441b492a96c73304fe Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 5 Feb 2026 13:36:49 +0900 Subject: [PATCH 1/3] Include detailed gemspec vs. lockfile dependency discrepancies --- bundler/lib/bundler/errors.rb | 38 +++++++- bundler/lib/bundler/lazy_specification.rb | 2 +- bundler/spec/bundler/errors_spec.rb | 104 ++++++++++++++++++++++ bundler/spec/install/failure_spec.rb | 38 ++++++++ 4 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 bundler/spec/bundler/errors_spec.rb diff --git a/bundler/lib/bundler/errors.rb b/bundler/lib/bundler/errors.rb index d8df4d6ec5c5..2b29f08b706d 100644 --- a/bundler/lib/bundler/errors.rb +++ b/bundler/lib/bundler/errors.rb @@ -265,14 +265,46 @@ def message class InvalidArgumentError < BundlerError; status_code(40); end class IncorrectLockfileDependencies < BundlerError - attr_reader :spec + attr_reader :spec, :actual_dependencies, :lockfile_dependencies - def initialize(spec) + def initialize(spec, actual_dependencies = nil, lockfile_dependencies = nil) @spec = spec + @actual_dependencies = actual_dependencies + @lockfile_dependencies = lockfile_dependencies end def message - "Bundler found incorrect dependencies in the lockfile for #{spec.full_name}" + msg = "Bundler found incorrect dependencies in the lockfile for #{spec.full_name}\n" + + if @actual_dependencies && @lockfile_dependencies + msg << "\n" + msg << "The gemspec for #{spec.full_name} specifies the following dependencies:\n" + if @actual_dependencies.empty? + msg << " (none)\n" + else + @actual_dependencies.sort_by(&:to_s).each do |dep| + msg << " #{dep.to_s}\n" + end + end + + msg << "\n" + msg << "However, the lockfile has the following dependencies recorded:\n" + if @lockfile_dependencies.empty? + msg << " (none)\n" + else + @lockfile_dependencies.sort_by(&:to_s).each do |dep| + msg << " #{dep.to_s}\n" + end + end + + msg << "\n" + msg << "This discrepancy may be caused by manually editing the lockfile.\n" + msg << "Please run `bundle install` to regenerate the lockfile with correct dependencies." + else + msg << "\nPlease run `bundle install` to regenerate the lockfile." + end + + msg end status_code(41) diff --git a/bundler/lib/bundler/lazy_specification.rb b/bundler/lib/bundler/lazy_specification.rb index 786dbcae6586..46b1e905d398 100644 --- a/bundler/lib/bundler/lazy_specification.rb +++ b/bundler/lib/bundler/lazy_specification.rb @@ -262,7 +262,7 @@ def validate_dependencies(spec) spec.dependencies = dependencies else if !source.is_a?(Source::Path) && spec.runtime_dependencies.sort != dependencies.sort - raise IncorrectLockfileDependencies.new(self) + raise IncorrectLockfileDependencies.new(self, spec.runtime_dependencies, dependencies) end end end diff --git a/bundler/spec/bundler/errors_spec.rb b/bundler/spec/bundler/errors_spec.rb new file mode 100644 index 000000000000..f88f8497e7e6 --- /dev/null +++ b/bundler/spec/bundler/errors_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::IncorrectLockfileDependencies do + describe "#message" do + let(:spec) do + double("LazySpecification", full_name: "rubocop-1.82.0") + end + + context "without dependency details" do + subject { described_class.new(spec) } + + it "provides a basic error message" do + expect(subject.message).to include("Bundler found incorrect dependencies in the lockfile for rubocop-1.82.0") + expect(subject.message).to include("Please run `bundle install` to regenerate the lockfile.") + end + end + + context "with dependency details" do + let(:actual_dependencies) do + [ + Gem::Dependency.new("json", [">= 2.3", "< 4.0"]), + Gem::Dependency.new("parallel", ["~> 1.10"]), + Gem::Dependency.new("parser", [">= 3.3.0.2"]), + ] + end + + let(:lockfile_dependencies) do + [ + Gem::Dependency.new("json", [">= 2.3", "< 3.0"]), + Gem::Dependency.new("parallel", ["~> 1.10"]), + Gem::Dependency.new("parser", [">= 3.2.0.0"]), + ] + end + + subject { described_class.new(spec, actual_dependencies, lockfile_dependencies) } + + it "provides a detailed error message showing the discrepancy" do + message = subject.message + + expect(message).to include("Bundler found incorrect dependencies in the lockfile for rubocop-1.82.0") + expect(message).to include("The gemspec for rubocop-1.82.0 specifies the following dependencies:") + expect(message).to include("json (>= 2.3, < 4.0)") + expect(message).to include("parallel (~> 1.10)") + expect(message).to include("parser (>= 3.3.0.2)") + expect(message).to include("However, the lockfile has the following dependencies recorded:") + expect(message).to include("json (>= 2.3, < 3.0)") + expect(message).to include("parser (>= 3.2.0.0)") + expect(message).to include("This discrepancy may be caused by manually editing the lockfile.") + expect(message).to include("Please run `bundle install` to regenerate the lockfile with correct dependencies.") + end + end + + context "when gemspec has dependencies but lockfile has none" do + let(:actual_dependencies) do + [ + Gem::Dependency.new("myrack-test", ["~> 1.0"]), + ] + end + + let(:lockfile_dependencies) { [] } + + subject { described_class.new(spec, actual_dependencies, lockfile_dependencies) } + + it "shows that lockfile has no dependencies" do + message = subject.message + + expect(message).to include("The gemspec for rubocop-1.82.0 specifies the following dependencies:") + expect(message).to include("myrack-test (~> 1.0)") + expect(message).to include("However, the lockfile has the following dependencies recorded:") + expect(message).to include("(none)") + end + end + + context "when gemspec has no dependencies but lockfile has some" do + let(:actual_dependencies) { [] } + + let(:lockfile_dependencies) do + [ + Gem::Dependency.new("unexpected", ["~> 1.0"]), + ] + end + + subject { described_class.new(spec, actual_dependencies, lockfile_dependencies) } + + it "shows that gemspec has no dependencies" do + message = subject.message + + expect(message).to include("The gemspec for rubocop-1.82.0 specifies the following dependencies:") + expect(message).to include("(none)") + expect(message).to include("However, the lockfile has the following dependencies recorded:") + expect(message).to include("unexpected (~> 1.0)") + end + end + end + + describe "#status_code" do + let(:spec) { double("LazySpecification", full_name: "test-1.0.0") } + subject { described_class.new(spec) } + + it "returns 41" do + expect(subject.status_code).to eq(41) + end + end +end diff --git a/bundler/spec/install/failure_spec.rb b/bundler/spec/install/failure_spec.rb index 2c2773e84917..0d260edb514a 100644 --- a/bundler/spec/install/failure_spec.rb +++ b/bundler/spec/install/failure_spec.rb @@ -48,4 +48,42 @@ end end end + + context "when lockfile dependencies don't match the gemspec" do + before do + build_repo4 do + build_gem "myrack", "1.0.0" do |s| + s.add_dependency "myrack-test", "~> 1.0" + end + + build_gem "myrack-test", "1.0.0" + end + + gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + # First install to generate lockfile + bundle :install + + # Manually edit lockfile to have incorrect dependencies + lockfile_content = File.read(bundled_app_lock) + # Remove the myrack-test dependency from myrack + lockfile_content.gsub!(/^ myrack \(1\.0\.0\)\n myrack-test \(~> 1\.0\)\n/, " myrack (1.0.0)\n") + File.write(bundled_app_lock, lockfile_content) + end + + it "reports the mismatch with detailed information" do + bundle :install, raise_on_error: false, env: { "BUNDLE_FROZEN" => "true" } + + expect(err).to include("Bundler found incorrect dependencies in the lockfile for myrack-1.0.0") + expect(err).to include("The gemspec for myrack-1.0.0 specifies the following dependencies:") + expect(err).to include("myrack-test (~> 1.0)") + expect(err).to include("However, the lockfile has the following dependencies recorded:") + expect(err).to include("(none)") + expect(err).to include("This discrepancy may be caused by manually editing the lockfile.") + expect(err).to include("Please run `bundle install` to regenerate the lockfile with correct dependencies.") + end + end end From 09477fef9ef64fb39bf002e6fcc031adbdef41c1 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 13 Feb 2026 16:05:26 +0900 Subject: [PATCH 2/3] bin/rubocop -a --only Lint/RedundantStringCoercion --- bundler/lib/bundler/errors.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundler/lib/bundler/errors.rb b/bundler/lib/bundler/errors.rb index 2b29f08b706d..c20aa3b7dcc6 100644 --- a/bundler/lib/bundler/errors.rb +++ b/bundler/lib/bundler/errors.rb @@ -283,7 +283,7 @@ def message msg << " (none)\n" else @actual_dependencies.sort_by(&:to_s).each do |dep| - msg << " #{dep.to_s}\n" + msg << " #{dep}\n" end end @@ -293,7 +293,7 @@ def message msg << " (none)\n" else @lockfile_dependencies.sort_by(&:to_s).each do |dep| - msg << " #{dep.to_s}\n" + msg << " #{dep}\n" end end From f588fb63d6b35e767cc7712b94f53da6239e4a38 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 13 Feb 2026 18:28:26 +0900 Subject: [PATCH 3/3] Fixed failing examples --- bundler/spec/commands/install_spec.rb | 6 +++++- bundler/spec/lock/lockfile_spec.rb | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/bundler/spec/commands/install_spec.rb b/bundler/spec/commands/install_spec.rb index ae651bf981c7..a0ad433d3ec6 100644 --- a/bundler/spec/commands/install_spec.rb +++ b/bundler/spec/commands/install_spec.rb @@ -1658,7 +1658,11 @@ def run bundle "install", raise_on_error: false expect(exitstatus).to eq(41) - expect(err).to eq("Bundler found incorrect dependencies in the lockfile for myrack_middleware-1.0") + expect(err).to include("Bundler found incorrect dependencies in the lockfile for myrack_middleware-1.0") + expect(err).to include("The gemspec for myrack_middleware-1.0 specifies the following dependencies:") + expect(err).to include("myrack (= 0.9.1)") + expect(err).to include("However, the lockfile has the following dependencies recorded:") + expect(err).to include("(none)") end it "updates the lockfile when not frozen" do diff --git a/bundler/spec/lock/lockfile_spec.rb b/bundler/spec/lock/lockfile_spec.rb index dcefe9cc2a2e..a68a91dafed8 100644 --- a/bundler/spec/lock/lockfile_spec.rb +++ b/bundler/spec/lock/lockfile_spec.rb @@ -1608,7 +1608,11 @@ gem "myrack_middleware" G - expect(err).to eq("Bundler found incorrect dependencies in the lockfile for myrack_middleware-1.0") + expect(err).to include("Bundler found incorrect dependencies in the lockfile for myrack_middleware-1.0") + expect(err).to include("The gemspec for myrack_middleware-1.0 specifies the following dependencies:") + expect(err).to include("myrack (= 0.9.1)") + expect(err).to include("However, the lockfile has the following dependencies recorded:") + expect(err).to include("(none)") expect(the_bundle).not_to include_gems "myrack_middleware 1.0" end