◉RORK LABJP
●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●RORK — GPT-6.1 Sol (Sep 29) and Claude Sonnet 5.5 (Sep 28) are now in the model menu●RN 0.88 — React Native 0.88.x is scheduled for Oct 12, 2 days left●Q&A — People keep asking why AuthSession returns no result in Android standalone builds●WINDOWS — A walkthrough shows how to ship an iPhone app from Windows only, with no Mac●NEW — When only notifications fail in Expo Go: three checks to run first●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●RORK — GPT-6.1 Sol (Sep 29) and Claude Sonnet 5.5 (Sep 28) are now in the model menu●RN 0.88 — React Native 0.88.x is scheduled for Oct 12, 2 days left●Q&A — People keep asking why AuthSession returns no result in Android standalone builds●WINDOWS — A walkthrough shows how to ship an iPhone app from Windows only, with no Mac●NEW — When only notifications fail in Expo Go: three checks to run first
Articles/Dev Tools
⬡ Dev Tools/2026-09-21Intermediate

A build that stops with exit code 0 — the flag that silenced the log, and the check that let an empty file through

Two reports landed in the same week: eas build --local stopping with exit code 0 but produced no further output, and a zero-byte stub that never got embedded. Both came down to settings that reduce output. Here is how to audit your own scripts.

Rork577Expo213EAS Build20xcodebuildTroubleshooting41iOS115

If you export a Rork project to GitHub and move the build onto your own Mac, there is a particular kind of failure waiting for you. The last line in the terminal reads like this, and nothing follows it.

error: the following command failed with exit code 0 but produced no further output

I sat and looked at that line for a while. Zero has meant success for as long as I have used a shell. Seeing it next to the word error leaves you unsure which half of the sentence to believe.

In the same week a second report showed up. That one builds fine, installs fine, and then dies the instant the app opens on a device. Neither had anything to do with application code. Both came from a small setting made somewhere in the build path.

Exit code 0 is not "it worked" — it is "I had nothing to say"

The first report comes from a stable setup: Expo SDK 57 with React Native 0.86, running eas build --platform ios --local under Xcode 27. It stops while assembling the expo-modules-jsi xcframework, leaving only the line above (expo/expo#50311).

What the reporter found was that xcodebuild was being invoked with -quiet at that step. The flag is meant to suppress warnings and progress noise, but what it suppresses also includes the actual error output. From the outside, the child process looks like it finished without saying anything — hence that strange phrasing about a command that failed yet produced no further output.

Dropping -quiet brought the swallowed lines back, and after the fix the build made it all the way through submission to App Store Connect.

So the order of suspicion matters more than anything else here. Before you suspect your code, check whether something in that step is reducing output. Reverse the order and you can spend half a day hunting a bug that was never there.

When you reproduce it locally, keep the quiet output but give it somewhere to land.

# Drop -quiet only while reproducing, and send the output to a file instead of nowhere
set -o pipefail   # so tee does not swallow xcodebuild's exit status
 
xcodebuild \
  -project ios/YourApp.xcodeproj \
  -scheme YourApp \
  -configuration Release \
  -destination 'generic/platform=iOS' \
  build 2>&1 | tee "build-$(date +%Y%m%d-%H%M%S).log"
 
echo "exit=${PIPESTATUS[0]}"   # read xcodebuild's status, not tee's

Without set -o pipefail and ${PIPESTATUS[0]}, the status you end up testing belongs to tee. Which means a failed build reports zero. You would be building the same trap the original report fell into, by hand.

The second silence was a check that counted a zero-byte file as present

The second report is an app built through EAS for a physical device that crashes in dyld the moment it launches (expo/expo#50236). The log shows this.

Library not loaded: @rpath/ExpoModulesJSI.framework/ExpoModulesJSI

After three rounds of digging, the reporter traced it to the official build script create-stub-xcframework.sh, which checks whether the stub has already been produced using -f.

-f only asks whether a regular file exists. An empty one still counts. If an earlier build died partway and left a zero-byte file behind, the script decides the work is done, skips rebuilding, and the Embed Frameworks phase passes something with no substance inside it. The app assembles cleanly and fails on launch.

The fix was to change the test to -s.

# Before: present is good enough (a zero-byte file passes)
if [ -f "$STUB_PATH" ]; then
  echo "stub already built"; exit 0
fi
 
# After: present AND non-empty
if [ -s "$STUB_PATH" ]; then
  echo "stub already built ($(wc -c < "$STUB_PATH") bytes)"; exit 0
fi
 
# Safer still: clear the broken leftover before deciding anything
if [ -f "$STUB_PATH" ] && [ ! -s "$STUB_PATH" ]; then
  echo "found a zero-byte stub, removing it"
  rm -f "$STUB_PATH"
fi

One character apart, and completely different in meaning. Here are the tests worth keeping straight.

TestTrue whenFor build artifacts
-eSomething exists at the path (directories too)Far too weak for output checks
-fA regular file exists (zero bytes still passes)Lets leftovers from an aborted run through
-sExists and has a size greater than zeroWhere cache checks should start
-dA directory existsUse this for .xcframework and .app

A .xcframework or .app is a directory, so -s cannot measure it. Name the binary inside and check that instead.

# .xcframework is a directory, so look at the binary inside it
FW="build/ExpoModulesJSI.xcframework"
BIN=$(find "$FW" -name 'ExpoModulesJSI' -type f -size +0 2>/dev/null | head -1)
 
if [ -z "$BIN" ]; then
  echo "xcframework is present but empty — rebuilding" >&2
  rm -rf "$FW"
else
  echo "ok: $BIN ($(wc -c < "$BIN") bytes)"
fi

One more thing both issues share: a bot closed them automatically for insufficient reproduction steps, even though the steps and the root cause are written out in the body. I am citing the numbers here so that anyone searching the same symptom does not read "closed" as "solved".

A ten-minute audit of your own scripts

As an indie developer with several apps to keep shipping, I reach a point in every project where the wall of build output becomes tiresome and I add a -q or a 2>/dev/null. It feels like an improvement that day. Six months later it comes back as a failure with no visible cause. I have done this to my own release scripts more than once.

This is the order I work through now.

  1. Count the places where output is being reduced.
grep -rnE -- '-quiet|(^| )-q( |$)|2>\s*/dev/null|&>\s*/dev/null|> */dev/null' \
  scripts/ ios/ .github/ 2>/dev/null
  1. Of those, keep only the ones attached to commands that can fail. Silencing echo or which costs nothing. Silencing xcodebuild, pod install, gradle or codesign removes the only diagnostic you will get.

  2. Sweep the existence checks on build outputs.

grep -rnE '\[ +-f +"?\$' scripts/ ios/ 2>/dev/null
  1. For the silence you want to keep, build an escape hatch. Skip the screen, never skip the file.
LOG="/tmp/ios-build-$(date +%s).log"
if ! xcodebuild -quiet ... build > "$LOG" 2>&1; then
  echo "build failed. last 40 lines:" >&2
  tail -40 "$LOG" >&2          # quiet by default, loud on failure
  exit 1
fi
  1. Finally, look for zero-byte files sitting in your output directory.
find ios/build -type f -size 0 2>/dev/null

The value of these five steps is highest on a day when the build is working. Run them after something breaks and you no longer know which part used to be fine. If you want to double-check that the dependency versions themselves match what you assume, I wrote about that in Nine Expo patches in thirty days — finding out which one your app actually has, and the related assumption about what gets uploaded is covered in Adding .easignore means .gitignore is no longer read.

If you reduce output, give it one place to land

Reading the two reports side by side, what struck me is that nobody did anything wrong. -quiet is a reasonable way to shorten a log. -f is the plain way to ask whether a file exists. The trouble came from one thing only: the information that was removed had nowhere to go.

For a long time I treated a shorter log as an improvement in itself. That did not serve me well. Every line I removed was a line I did not have on the day something failed. I keep one rule now.

You may silence only the places that can speak again when they fail.

This is not specific to projects exported from Rork. On the way to the store, your logs move between your Mac, CI and EAS, and each move is a chance for them to land nowhere. Decide where they land each time.

Before your next build, open one line where you call xcodebuild and check only this: where does its output go when the command fails? If the answer is /dev/null, changing that to a file under /tmp is enough to start with. That is where I started, too.

If you find a silenced step I did not think of, I would be glad to hear about it — I am still filling in the gaps in my own scripts.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-09-07
Adding .easignore stops EAS from reading .gitignore — count what actually ships to the build
EAS Build decides what to upload from .gitignore, and the moment you add .easignore the two swap places. Here is how I count the bundled files before sending, plus a measured result: once a parent directory is excluded, an exclamation mark cannot bring a file back.
⬡ Dev Tools2026-09-05
Your Widget Extension Can Submit a BGTaskScheduler Request. It Just Cannot Register the Handler
Calling BGTaskScheduler from a widget extension compiles, submits, and returns true — and then nothing runs. Here is why registration belongs to the host app only, and how I moved refresh ownership back where it belongs across my wallpaper apps.
⬡ Dev Tools2026-08-20
A beta-SDK build can reach TestFlight, but it can't reach review
Builds made with a beta Xcode can be distributed through TestFlight, but they cannot be submitted for App Store review. Here is how to check which SDK produced your build, and how to protect your release profile in eas.json.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links