Rork で作ったアプリを GitHub へ書き出して、手元の Mac でビルドに移った方が最初に出会う種類の失敗があります。ターミナルに流れた最後の行が、これだけというものです。
error: the following command failed with exit code 0 but produced no further output
失敗コードが 0 という一行を前にして、私はしばらく画面を眺めておりました。0 は慣例として成功を指します。その 0 が error という語と並んでいるのですから、どちらを信じればよいのか分からなくなります。
同じ週に、もう一つ似た報告が並んでいました。ビルド自体は通るのに、実機に入れて起動した瞬間に落ちる、というものです。どちらも原因はコードの中ではなく、ビルドを進めるための小さな設定の側にありました。
exit code 0 は「成功」ではなく「何も言えませんでした」の印です
ひとつめは、Expo SDK 57 / React Native 0.86 という安定版の構成で、Xcode 27 のもとで eas build --platform ios --local を走らせたときの報告です。expo-modules-jsi の xcframework を組み立てる段で、先ほどの一行だけを残して止まります(expo/expo#50311)。
報告者が突き止めた原因は、その段で呼ばれる xcodebuild に -quiet が渡されていたことでした。-quiet は警告と進捗を抑えるための指定ですが、抑える対象に実際のエラー出力まで含まれてしまいます。外側の仕組みから見ると、子プロセスは何も言わずに終わったように見えます。だから「コマンドは失敗したが、それ以上の出力は生まれなかった」という、あの奇妙な文面になるのです。
-quiet を外して回し直すと、握りつぶされていた行がそのまま出てきて、修正後は App Store Connect への提出まで通ったと書かれています。
読者の皆さまに最初にお伝えしたいのは、この一行が出たときに疑う順番です。コードを疑う前に、その段で出力を減らしている設定があるかどうかを先に見ます。順番を逆にすると、存在しないバグを半日探すことになります。
手元で確かめるときは、静かなままログだけを残す形にしておくと後が楽になります。
# 失敗の再現時だけ -quiet を外し、出力は捨てずにファイルへ逃がす
# tee を挟むので画面にも流れ、あとから grep もできます
set -o pipefail # xcodebuild の終了コードを tee で握りつぶさないための一行です
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]}" # tee ではなく xcodebuild 側の終了コードを見ますset -o pipefail と ${PIPESTATUS[0]} の二つがない状態でパイプを繋ぐと、判定されるのは tee の終了コードになります。つまり、ビルドが落ちても 0 が返ります。冒頭の報告とよく似た形を、自分の手で作り出してしまうことになるのです。
もう一つの沈黙は、0 バイトのファイルを「ある」と数えた存在チェックでした
ふたつめは、実機向けの EAS ビルドで作ったアプリが、起動した瞬間に dyld のクラッシュで終わるという報告です(expo/expo#50236)。ログに出るのは次の一行です。
Library not loaded: @rpath/ExpoModulesJSI.framework/ExpoModulesJSI
報告者は三段階の調査の末に、公式のビルドスクリプト create-stub-xcframework.sh に辿り着いています。そこでは、すでにスタブが作られているかどうかを -f で確かめていました。
-f は「通常ファイルとして存在するか」だけを見ます。中身が空でも、存在していれば真です。前の回のビルドが途中で落ちて 0 バイトのファイルを残していた場合、スクリプトは「もう作ってある」と判断して作り直さず、Embed Frameworks の段でも実体のないものが素通りします。アプリは出来上がり、実機で起動した瞬間に読み込めないと言われます。
修正は、判定を -s に変えるだけでした。
# 変更前: 存在すれば通す(0 バイトでも通ってしまいます)
if [ -f "$STUB_PATH" ]; then
echo "stub already built"; exit 0
fi
# 変更後: 存在し、かつ空でないときだけ通します
if [ -s "$STUB_PATH" ]; then
echo "stub already built ($(wc -c < "$STUB_PATH") bytes)"; exit 0
fi
# さらに安全側へ寄せるなら、壊れた残骸を先に片づけます
if [ -f "$STUB_PATH" ] && [ ! -s "$STUB_PATH" ]; then
echo "found a zero-byte stub, removing it"
rm -f "$STUB_PATH"
fi一文字の差ですが、意味はまったく違います。よく使う判定の違いを並べておきます。
| 判定 | 真になる条件 | ビルド生成物に使うとき |
|---|---|---|
-e | 何らかの形で存在する(ディレクトリでも真) | 成果物の確認には弱すぎます |
-f | 通常ファイルとして存在する(0 バイトでも真) | 中断した前回の残骸を通してしまいます |
-s | 存在し、かつサイズが 0 より大きい | キャッシュ判定はここから始めます |
-d | ディレクトリとして存在する | .xcframework や .app はこちらで見ます |
.xcframework や .app はディレクトリですから、-s では測れません。中の実体を名指しして確かめるほうが確実です。
# .xcframework はディレクトリなので、中のバイナリを直接見ます
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この二件には、Bot が「再現手順が足りない」として自動的に閉じたという共通点もあります。再現手順も原因も本文に書かれているのに閉じられていますので、同じ症状で検索した方が「解決済み」と読み違えないよう、issue 番号を添えて残しておきます。
自分のスクリプトを 10 分で棚卸しする
個人開発でアプリを何本か抱えていますと、ビルドを回すたびに流れる大量の行が邪魔になり、どこかの時点で -q や 2>/dev/null を足したくなります。私自身、配信まわりのスクリプトにそうした指定を足したことが何度もあります。足した当日は快適ですが、半年後に原因の見えない失敗として返ってきます。
順番はこうしています。
- 出力を減らしている箇所を数えます。
grep -rnE -- '-quiet|(^| )-q( |$)|2>\s*/dev/null|&>\s*/dev/null|> */dev/null' \
scripts/ ios/ .github/ 2>/dev/null-
見つかった行のうち、失敗しうるコマンドに付いているものだけを残します。
echoやwhichを黙らせるのは無害です。xcodebuild・pod install・gradle・codesignの類は、黙らせた瞬間に診断の手がかりを失います。 -
成果物の存在チェックを洗います。
grep -rnE '\[ +-f +"?\$' scripts/ ios/ 2>/dev/null- 残したい沈黙には、逃がし先を作ります。画面に出さない代わりに、必ずファイルへ落とします。
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 # 黙らせても、落ちたときだけは喋らせます
exit 1
fi- 最後に、0 バイトのファイルが成果物ディレクトリに残っていないかを見ます。
find ios/build -type f -size 0 2>/dev/nullこの 5 手順は、ビルドが通っている日にこそ回す価値があります。壊れてから探すと、どこまでが正常だったのかが分からなくなるためです。依存そのものの版が思い込みとずれていないかは、30日で9回出た Expo のパッチのうち、自分のアプリに入っているのはどれかを確かめるに書きました。アップロードされる範囲の思い込みについては、.easignore を置くと .gitignore は読まれなくなりますが近い話になります。
出力を減らすなら、逃がし先を必ず一つ作ります
二つの報告を並べて気づいたのは、どちらも「間違ったことをした」わけではない、ということでした。-quiet は行数を減らすための正当な指定ですし、-f はファイルの存在を確かめる素直な書き方です。問題が起きたのは、減らした情報の行き先を用意していなかった一点に尽きます。
最初のうち、私はログを短くすることをそのまま改善だと考えておりました。結果は芳しくありませんでした。短くした分だけ、失敗したときに見るものが無くなっていたのです。いまは一つだけ線を引いています。
黙らせてよいのは、落ちたときに喋り直せるようにしてある箇所だけです。
この線引きは、Rork から書き出したプロジェクトに限った話ではありません。ストアへ出すまでの道のりでは、手元の Mac・CI・EAS と、ログの置き場所が何度も変わります。変わるたびに、どこへ逃がしているのかを一つ決めておく必要があります。
次にビルドを回すときは、xcodebuild を呼んでいる行を一つ開いて、失敗した場合にその出力がどこへ行くのかだけを確かめてみてください。行き先が /dev/null になっていたら、そこを /tmp のファイルに書き換えるところから始めていただければと思います。私もそこから始めました。
最後までお読みくださり、ありがとうございました。