Skip to content

BUNDLE_FROZEN=1 bundle install proceeds with install and exits 0 with many different forms of invalid lockfile #9549

@eapache-opslevel

Description

@eapache-opslevel

I was tracking down an issue I had seen locally, and then I noticed a similar report yesterday in #9546, so I broadened the scope of my investigation somewhat.

Describe the problem as clearly as you can

My original issue: if BUNDLE_FROZEN=1 bundle install is run on a lockfile that contains unexpected/orphan entries in the CHECKSUMS section (i.e. gem listed in CHECKSUMS but not in GEM), bundler will print Cannot write a changed lockfile while frozen. to stderr, then proceeds anyway with the install, and exits 0.

Upon further investigation (with Claude) there are quite a number of similar other cases:

  • orphaned entry in the GEM section (i.e. not in DEPENDENCIES and not a transitive dep)
  • duplicate entry in the GEM section (two different versions listed of the same gem)
  • PLATFORM entries listed in the wrong order

And probably others that I haven't looked at... claude suggests

  any other lockfile content that to_lock would normalize away — stale GIT/PATH source blocks
  for gems no longer used, ordering of dependencies under a source, missing trailing newline, etc. — should fall
  into the same trap, since they all go through the same final write path

Did you try upgrading rubygems & bundler?

Reproduced on latest git commit 5869518.

Post steps to reproduce the problem

  FROM rubylang/ruby:4.0.4

  ARG BUNDLER_VERSION=4.0.11

  RUN gem install bundler:${BUNDLER_VERSION} --no-document

  RUN useradd --create-home --shell /bin/bash app \
   && mkdir -p /app && chown app:app /app
  USER app
  WORKDIR /app

  RUN cat > Gemfile <<'EOF'
  source "https://rubygems.org"
  gem "rake", "13.2.1"
  EOF

  RUN bundle config set --local path 'vendor/bundle' \
   && bundle install \
   && bundle lock --add-checksums \
   && cp Gemfile.lock Gemfile.lock.bak

  # Inject an orphan gem ("json") into the lockfile: it is listed in GEM and
  # CHECKSUMS, but is NOT in DEPENDENCIES and is NOT a transitive dependency of
  # rake. The lockfile is therefore self-inconsistent — `to_lock` would prune it.
  RUN sed -i \
        -e '/^    rake (13.2.1)$/a\    json (2.7.0)' \
        -e '/^  rake (13.2.1) sha256=/a\  json (2.7.0) sha256=1f64f8b32a3a570286a32fb203b863b6233aa1da6193ab0648fe7e35aa17fb09' \
        Gemfile.lock

  CMD set +e; \
      echo "=== BUNDLE_FROZEN=${BUNDLE_FROZEN} ==="; echo; \
      echo "=== bundle --version ==="; bundle --version; echo; \
      echo "=== Gemfile.lock diff ==="; diff -u Gemfile.lock.bak Gemfile.lock; echo; \
      echo "=== bundle check ===";     bundle check; echo "=> exit=$?"; echo; \
      echo "=== bundle install ===";   bundle install; echo "=> exit=$?"

Build and run:

  docker build -t bundler-repro .
  docker run --rm --env BUNDLE_FROZEN=1 bundler-repro

Which command did you run?

BUNDLE_FROZEN=1 bundle install

What were you expecting to happen?

bundler should refuse to install, and exit non-zero in frozen mode if the lockfile is invalid in some way

(Or, arguably, some of these differences could be accepted as cosmetic, and not produce any stderr.)

What happened instead?

Cannot write a changed lockfile while frozen.
Bundle complete! 1 Gemfile dependency, 1 gem now installed.
Bundled gems are installed into `./vendor/bundle`
=> exit=0

Fix?

Claude suggested the following fix, which I will include for completeness, though I am not yet familiar enough with the bundler code base to validate it:

  One change closes all of them: in definition.rb:415-418, replace `Bundler.ui.error "Cannot write a changed 
  lockfile while frozen."; return` with a `raise ProductionError`. That turns the existing canonical-form diff (which
  bundler is already computing) into the authoritative frozen-mode check, instead of having a weaker Gemfile-only
  comparison gating things and a real check that only logs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions