diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index ac3792938659..d2cc9cbdd4f3 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -410,7 +410,7 @@ def write_lock(file, preserve_unknown_sections) updating_major = locked_major < current_major end - preserve_unknown_sections ||= !updating_major && (Bundler.frozen_bundle? || !(unlocking? || @unlocking_bundler)) + preserve_unknown_sections ||= Bundler.frozen_bundle? || (!updating_major && !(unlocking? || @unlocking_bundler)) if File.exist?(file) && lockfiles_equal?(@lockfile_contents, contents, preserve_unknown_sections) return if Bundler.frozen_bundle? @@ -419,8 +419,10 @@ def write_lock(file, preserve_unknown_sections) end if Bundler.frozen_bundle? - Bundler.ui.error "Cannot write a changed lockfile while frozen." - return + msg = lockfile_changes_summary("frozen mode is set") || + "Your lockfile needs to be updated, but it can't be because frozen mode is set.\n\n" \ + "Run `bundle install` elsewhere and add the updated #{SharedHelpers.relative_lockfile_path} to version control." + raise ProductionError, msg end # Convert to \r\n if the existing lock has them, i.e., Windows with diff --git a/lib/bundler/lockfile_generator.rb b/lib/bundler/lockfile_generator.rb index 8ed501396420..f2b9afb388f6 100644 --- a/lib/bundler/lockfile_generator.rb +++ b/lib/bundler/lockfile_generator.rb @@ -103,6 +103,11 @@ def add_section(name, value) end def bundler_checksum + # In frozen mode the lockfile can't change, so reproduce whatever bundler + # entry is already locked instead of recording one for the running bundler + # version, which may legitimately differ from the locked one. + return locked_bundler_checksum if Bundler.frozen_bundle? + # `.dev` versions and `SKIP_BUNDLER_CHECKSUM` are deliberate opt-outs (used # by Bundler/RubyGems' own development and release tasks): never record a # checksum for Bundler itself in those cases. @@ -138,5 +143,16 @@ def bundled_with_changing? locked_gems.bundler_version != definition.bundler_version_to_lock end + + def locked_bundler_checksum + locked_version = definition.locked_gems&.bundler_version + return [] unless locked_version + + metadata_source = definition.sources.metadata_source + locked_spec = LazySpecification.new("bundler", locked_version, Gem::Platform::RUBY, metadata_source) + return [] if metadata_source.checksum_store.missing?(locked_spec) + + [metadata_source.checksum_store.to_lock(locked_spec)] + end end end diff --git a/spec/cache/git_spec.rb b/spec/cache/git_spec.rb index f0976ecac7c9..f0bff333cb4b 100644 --- a/spec/cache/git_spec.rb +++ b/spec/cache/git_spec.rb @@ -402,7 +402,7 @@ G lockfile <<~L GIT - remote: #{git_path}/ + remote: #{git_path} revision: #{locked_revision} specs: foo (1.0) diff --git a/spec/commands/cache_spec.rb b/spec/commands/cache_spec.rb index b33a5a386c74..ad0afe390eb7 100644 --- a/spec/commands/cache_spec.rb +++ b/spec/commands/cache_spec.rb @@ -349,15 +349,15 @@ GEM remote: https://gem.repo4/ specs: - foo (1.0.0) bar (1.0.0) + foo (1.0.0) PLATFORMS #{lockfile_platforms} DEPENDENCIES - foo bar + foo BUNDLED WITH #{Bundler::VERSION} diff --git a/spec/install/deploy_spec.rb b/spec/install/deploy_spec.rb index 5bdb8f6194f5..c8947393c089 100644 --- a/spec/install/deploy_spec.rb +++ b/spec/install/deploy_spec.rb @@ -173,6 +173,75 @@ end.not_to change { bundled_app_lock.mtime } end + it "explodes if regenerating the lockfile would change it" do + lockfile lockfile. + sub(" myrack (1.0.0)", " myrack-obama (1.0)\n myrack (1.0.0)"). + sub(/^ myrack \(1\.0\.0\) sha256=\S+$/) {|line| "#{line}\n #{checksum_to_lock(gem_repo1, "myrack-obama", "1.0")}" } + + bundle :install, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false + expect(err).to include("Your lockfile needs to be updated, but it can't be because frozen mode is set") + expect(last_command).to be_failure + end + + it "explodes on `bundle check` if the lockfile contains a gem bundler would prune" do + lockfile lockfile.sub(" myrack (1.0.0)", " myrack (1.0.0)\n myrack-obama (1.0)") + + bundle :check, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false + expect(err).to include("but can't be updated because frozen mode is set") + expect(last_command).to be_failure + end + + it "works when the lockfile includes a checksum entry for bundler itself" do + lockfile <<~L + GEM + remote: https://gem.repo1/ + specs: + myrack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + + CHECKSUMS + bundler (#{Bundler::VERSION}) sha256=#{"a" * 64} + #{checksum_to_lock gem_repo1, "myrack", "1.0.0"} + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle :install, env: { "BUNDLE_FROZEN" => "true" } + expect(err).to be_empty + end + + it "explodes if the lockfile checksum entry for bundler does not match the BUNDLED WITH version" do + lockfile <<~L + GEM + remote: https://gem.repo1/ + specs: + myrack (1.0.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + + CHECKSUMS + bundler (4.0.16) sha256=#{"a" * 64} + #{checksum_to_lock gem_repo1, "myrack", "1.0.0"} + + BUNDLED WITH + #{Bundler::VERSION} + L + + bundle :install, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false + expect(err).to include("Your lockfile needs to be updated, but it can't be because frozen mode is set") + expect(last_command).to be_failure + end + it "explodes with the `deployment` setting if you make a change and don't check in the lockfile" do gemfile <<-G source "https://gem.repo1" diff --git a/spec/install/gemfile/platform_spec.rb b/spec/install/gemfile/platform_spec.rb index c28af3d4abfe..87e9dcf77ec0 100644 --- a/spec/install/gemfile/platform_spec.rb +++ b/spec/install/gemfile/platform_spec.rb @@ -54,7 +54,7 @@ it "pulls the pure ruby version on jruby if the java platform is not present in the lockfile and bundler is run in frozen mode", :jruby_only do lockfile <<-G GEM - remote: https://gem.repo1 + remote: https://gem.repo1/ specs: platform_specific (1.0)