Skip to content

Run pods through bundler when the project pins them - #148

Merged
janicduplessis merged 6 commits into
mainfrom
feat/issue-137-bundler-pods
Aug 31, 2026
Merged

Run pods through bundler when the project pins them#148
janicduplessis merged 6 commits into
mainfrom
feat/issue-137-bundler-pods

Conversation

@janicduplessis

@janicduplessis janicduplessis commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Description

doctor prints bundle exec pod install when the project has a Gemfile, while the engine always spawned plain pod install (#137). For a project whose Gemfile.lock pins cocoapods, the CocoaPods that rewrites ios/Podfile.lock during a Stim build can then be a different one than the repo pins -- the lockfile churn and rvm activation trouble #133/#135 hit in the e2e fixture.

Stim must not edit tracked project files, and bundler rewrites Gemfile.lock whenever it resolves, so every bundler spawn here runs with BUNDLE_FROZEN=true and the check runs --dry-run. To be precise about the scope of that claim: BUNDLE_FROZEN protects Gemfile.lock specifically. bundle install still installs gems, and when the project's own committed .bundle/config points BUNDLE_PATH inside the tree -- the React Native template ships vendor/bundle -- that is where they land. That is the project's dependency state written by the project's own toolchain config, the same class as Pods/ and node_modules/, and the template gitignores it (--carry-ignored carries it into worktrees). I deliberately did not redirect BUNDLE_PATH to a Stim directory: the user's own bundle exec has to find the same gems. The run now prints a dim pods note when the resolved bundle path is inside the project.

What bundler 4.0.8 does to a Gemfile.lock that no longer matches its Gemfile (measured, not assumed)
command Gemfile.lock
bundle check rewritten
bundle check --dry-run untouched
bundle exec <cmd> rewritten
BUNDLE_FROZEN=true bundle exec <cmd> untouched, exits 1

Solution

The pod step takes the first rung that applies:

  1. Gemfile + a Gemfile.lock that resolves cocoapods -- bundle check --dry-run at the Gemfile, then bundle install only if that reports missing gems, then bundle exec pod install in ios/.
  2. Gemfile with no Gemfile.lock -- plain pod install, exactly as before. bundle install there would CREATE a lockfile in the checkout, which is the write Stim will not do; there is no pin to honor anyway.
  3. A Gemfile.lock that pins something else (fastlane-only bundles are common in RN repos) -- plain pod install. Nothing was pinned for pods, so nothing is gained by running bundle exec pod install and failing where plain pod worked. No note: there is no pin to report.
  4. No bundle on PATH -- plain pod install plus one dim pods note naming the pin it could not use. A note, not a failure.

A drifted Gemfile (one that no longer matches its lockfile) fails the build rather than falling back to unpinned pods -- silently building with a different CocoaPods than the lockfile pins is the bug class this feature exists to kill -- and the failure carries the bundle install remedy.

Rung 1's test is a four-space-indented cocoapods ( spec line in Gemfile.lock -- that is bundler's format for a resolved spec (six spaces marks a spec's dependencies, two marks DEPENDENCIES), so it catches cocoapods pulled in transitively, which DEPENDENCIES would miss. An unreadable or malformed lockfile is treated as "not pinned" rather than an error. The rule lives in engine/bundler.ts and every surface that prints a pod command reads it: doctor's two CocoaPods remedies, worktree create's carried-Pods warning, and the xcodebuild "sandbox is not in sync" remedy. A fastlane-only repo is told pod install everywhere; a pinned repo is told bundle exec pod install everywhere.

All three bundler spawns keep podEnv() (the .ruby-version PATH/GEM_HOME handling) and add BUNDLE_GEMFILE. A Gemfile that has drifted from its lockfile now fails the run with a remedy pointing at bundle install instead of having a tracked file updated underneath you; gems bundler cannot install keep the existing .ruby-version hint. The pods phase line names the command that ran, and stim guide errors describes the ladder under STIM_DEPS_FAILED. STIM_DEPS_FAILED itself and the extracted [!]/ruby diagnostics are unchanged.

Test plan

Real tools, stim ios on a bare RN 0.87 worktree with ios/Pods and vendor/ removed, which drives rung 1 end to end -- bundle check --dry-run (missing gems) -> bundle install -> bundle exec pod install -> build -> launch:

gems        still running (30s): Installing activesupport 7.2.3.2
pods        still running (30s): [Hermes] Cache hit: copying hermes-ios-...
  pods        ios/Pods/Manifest.lock is missing (pods have never been installed here) -> installed with `bundle exec pod install` (43.3s)
  build       ok (28.5s)
  verify      ready: bundle loaded, stable for 3s, process alive (3.9s total)

git status --porcelain in that worktree was empty before and after, and git diff -- Gemfile.lock was empty: 45 gems and 86 pods installed without touching a tracked file. The gems did land in the worktree, in vendor/bundle per the fixture's own .bundle/config -- gitignored, hence the clean status, and exactly what the new dim note now reports.

  • node test/e2e/native/run-native-e2e.mjs --framework bare --platform ios -- PASS. Worth knowing: that e2e never reaches this code, because worktree create --carry-ignored carries a matching ios/Pods and the pod step is skipped. Hence the manual run above.
  • Unit tests cover each rung with a mocked spawn: a cocoapods-pinned project uses check -> exec, a failing check inserts bundle install, a fastlane-only lock and a Gemfile without a lockfile stay on plain pod install (asserting the fallback env carries no BUNDLE_FROZEN/BUNDLE_GEMFILE), a missing bundle falls back with the note, and each new failure returns STIM_DEPS_FAILED with its remedy. engine-bundler.test.ts pins the lockfile parse itself: CRLF, a GIT-sourced spec, a transitive cocoapods dependency line with no spec of its own (negative), a CHECKSUMS entry alone (negative), and a lockfile that is a directory. A guide contract test ties the STIM_DEPS_FAILED text to the argv the engine actually spawns, so the two cannot drift again. The four-space spec match was also checked against a real bundle lock output pinning cocoapods 1.17.0.

Fixes #137

A project root with both a Gemfile and a Gemfile.lock now gets `bundle check
--dry-run`, `bundle install` only when gems are missing, then `bundle exec pod
install`, so the CocoaPods the lockfile pins is the one that writes Podfile.lock.

Bundler resolves and writes Gemfile.lock from check, install and even exec, so
every bundler spawn runs with BUNDLE_FROZEN and the check runs --dry-run: stim
never writes into the project tree. A Gemfile with no lockfile, and a machine
with no `bundle` on PATH, stay on plain `pod install`.

Refs #137
A Gemfile+Gemfile.lock that pins fastlane and nothing else pinned nothing for
pods, so `bundle exec pod install` would fail where plain `pod install` worked.
Rung 1 now requires a cocoapods spec entry in Gemfile.lock, and doctor's pod
remedies read the same rule.

Refs #137
Moves bundlerPin into engine/bundler.ts so doctor, worktree's carried-Pods
warning and the xcodebuild sandbox-out-of-sync remedy print `bundle exec pod
install` only when Gemfile.lock resolves cocoapods.

Refs #137

Review fixes: report `bundle` (not CocoaPods) when the bundler branch hits
ENOENT, rewrite the no-pod-in-bundle remedy for the states that can reach it,
narrow the frozen-refusal match to bundler's own phrasing, label the check
spawn `gems`, and note the project-local BUNDLE_PATH `bundle install` fills.
A leading ~ is expanded by bundler to $HOME, so it never lands in the project;
resolve() would have read it as a directory named "~" and printed the
in-project note anyway. The note now also says whether BUNDLE_PATH came from
the environment or from .bundle/config, so nobody hunts for a file that is not
there.

Refs #137
@janicduplessis
janicduplessis marked this pull request as ready for review August 31, 2026 21:55
@janicduplessis
janicduplessis merged commit 0c636f6 into main Aug 31, 2026
5 checks passed
@janicduplessis
janicduplessis deleted the feat/issue-137-bundler-pods branch August 31, 2026 21:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

doctor recommends bundle exec pod install while the engine runs plain pod

1 participant