フェイルファスト(Fail-Fast)設計とは、必須の前提条件や成果物の欠落を検知した時点で処理を即座に停止させ、後続のサイレントな障害を防ぐ設計思想です。CI や単体テストがすべて 「緑(PASS)」 に染まっていても、それは「テストが対象コードを検証できた」ことを意味するに過ぎず、「成果物たるアプリケーションが正しく組み上がって起動できる」ことの保証にはなりません。

Swift Package Manager(SPM)で macOS デスクトップアプリケーション(SwiftUI)を開発し、シェルスクリプトで .app バンドルを組み立てていた際、単体テストをパスし、ビルドスクリプトも正常終了(exit 0)したにもかかわらず、生成されたアプリを起動した瞬間にクラッシュする問題に直面しました。特に、Claude Code などの AI エージェントにスクリプト作成やビルドタスクを委譲している開発現場では、エージェントが終了ステータス(exit 0)を盲信してサイレント障害を見落とすリスクが増大します。

原因は、SPM のリソースバンドル命名変化に対して、スクリプト内の存在確認 if [ -d ] が未検出時に何もしなかったため、成果物の欠落が黙って隠蔽されていたことにありました。以下では、このサイレント障害の機序を解明するとともに、「見つからなければ何もしない」甘い条件分岐を排して即座に停止させるフェイルファスト化、および単体テスト通過とアプリ組み上がりの分離検証という教訓を整理します。

なぜ存在確認の if は失敗を隠蔽したのか:SPM の命名仕様とサイレント障害

SPM におけるリソースバンドル生成仕様と命名規則

Swift Package Manager(SPM)で画像や設定ファイルなどのアセットを扱う場合、SE-0271(Package Manager Resources)1 に従ってターゲットごとにリソースを宣言します。SPM はリソースを含むターゲットをビルドする際、Foundation スタイルのバンドルを自動的に生成します。このバンドルの命名規則は <PackageName>_<TargetName>.bundle の形式をとります。

一方、macOS のデスクトップアプリケーション(.app)は、Apple のドキュメント2 で規定されているように、決まった階層構造(Bundle Structure)を持ちます。実行ファイル本体は Contents/MacOS/ 配下に格納され、アセットや SPM が生成したリソースバンドルは Contents/Resources/ 配下に配置されなければなりません。Swift コード内で Bundle.module を呼び出すと、実行時にこの Contents/Resources/ 配下を探索してバンドルをロードします。

SPM で macOS アプリを開発する場合、現行の SPM には .app バンドルを直接出力するコマンドが存在しないため、CI/CD パイプラインでのヘッドレス実行や自動化を目的として、Xcode のビルドシステムを使わずにシェルスクリプト(build_app.sh)を用いて swift build の出力物から .app ディレクトリを組み立てる手法がしばしば採られます。しかし、自作のパッケージングスクリプトでは、以下のようなコードでリソースを探索・コピーしていました。

引用元: nqounet/saga コミット f1d5c3e~1:build_app.sh

SPM_RESOURCES=$(find .build/release -iname "${APP_NAME}_${APP_NAME}.resources" | head -n 1)
if [ -d "${SPM_RESOURCES}" ]; then
    echo "==> Embedding resources..."
    cp -R "${SPM_RESOURCES}/" "${APP_DIR}/Contents/Resources/"
fi

このコードには2つの重大な乖離がありました。第一に、スクリプトは古い命名想定に基づいて ${APP_NAME}_${APP_NAME}.resources(今回の例では Saga_Saga.resources)を探していましたが、実際の Swift ツールチェーンが出力していたのは .build/out/Products/Release/SAGA_Saga.bundle(Package 名 SAGA + Target 名 Saga の .bundle)でした。第二に、.build/release はシンボリックリンクであり、末尾にスラッシュを付けない find .build/release ではリンク先が展開されず、探索対象から外れていました。

set -e をすり抜ける if [ -d ] の罠と未検出時の素通り

シェルスクリプトの先頭に set -e を宣言していても、上記のコードはエラーで停止しませんでした。

POSIX 仕様(IEEE Std 1003.1 §2.14)3 において、set -e は if や while、until に続くテスト複合リストの実行中は無効化されると明記されています。したがって、[ -d "${SPM_RESOURCES}" ] が偽(空文字に対する判定で終了コード 1)を返しても、シェルはスクリプトの実行を中断しません。条件分岐の判定式におけるエラーが素通りする挙動は、シェルスクリプトの典型的な落とし穴の一つです4。

さらに、このスクリプトには else ブロックが存在しませんでした。そのため、必須のリソースバンドルが 1 件も見つからなかったにもかかわらず、スクリプトは何のエラーも警告も出力せず、そのまま次の行の処理へと素通りしてしまいました。

その結果、生成された Saga.app/Contents/Resources/ の中身は、アイコンファイル(AppIcon.icns)のみという不完全な状態になりました。

引用元: 当リポジトリの一次計測ログ(hands-on-log.md §2)

Saga.app/Contents/Resources/:
AppIcon.icns

それにもかかわらず、スクリプト全体としては正常終了(exit 0)し、コンソールには 「Build Successful」 と表示されたのです。必須成果物の欠落が、スクリプトの甘い存在確認によって完全に隠蔽されていました。

起動時クラッシュと exit 1 によるフェイルファストへの転換

リソースバンドルが欠落した .app は、外見上は正常なアプリケーションパッケージとして作成されます。しかし、これを実行した瞬間に問題が露見します。

引用元: 当リポジトリの一次計測ログ(hands-on-log.md §3)

Saga/resource_bundle_accessor.swift:44: Fatal error: unable to find bundle named SAGA_Saga

アプリケーションの起動処理中に Bundle.module が初期化される際、Contents/Resources/ 配下に SAGA_Saga.bundle が見つからないため、自動生成されたリソースアクセサ(resource_bundle_accessor.swift)が Fatal error を発生させ、ウィンドウすら表示されずに即座に異常終了しました。

「見つかった場合だけコピーする」という if [ -d ] の防護的なコードは、オプショナルな設定ファイルやキャッシュの扱いとしては合理的であっても、欠落すれば起動すらできない必須成果物に対して適用すると、不一致や欠落という想定外の異常を検知できず、実行時の致命的なクラッシュへと問題を先送りしてしまいます。予防は想定した原因の空間に対して設計されますが、検収は結果の空間(必須成果物が確かに手元にあるか)に対して設計されなければなりません。

この問題を解決するため、build_app.sh に対して以下の改修を行いました。

引用元: nqounet/saga コミット f1d5c3e:build_app.sh 差分

-SPM_RESOURCES=$(find .build/release -iname "${APP_NAME}_${APP_NAME}.resources" | head -n 1)
+SPM_RESOURCES=$(find .build/release/ -maxdepth 1 -iname "*_${APP_NAME}.bundle" | head -n 1)
 if [ -d "${SPM_RESOURCES}" ]; then
     echo "==> Embedding resources..."
-    cp -R "${SPM_RESOURCES}/" "${APP_DIR}/Contents/Resources/"
+    cp -R "${SPM_RESOURCES}" "${APP_DIR}/Contents/Resources/"
+else
+    echo "Error: SPM resource bundle not found (app would crash at launch)"
+    exit 1
 fi

改修のポイントは2点です。

  1. ワイルドカード探索とシンボリックリンク追跡: find .build/release/ と末尾にスラッシュを付与してシンボリックリンクを確実に展開し、*_${APP_NAME}.bundle でパッケージ名の大文字・小文字の差異を吸収しました。
  2. 未検出時のフェイルファスト停止: else ブロックを設け、リソースバンドルが見つからない場合はエラーメッセージを STDERR に出力して即座に exit 1 で終了させました。

この改修により、万が一ツールチェーンの出力仕様やファイル名が再び変化したとしても、サイレントに壊れたアプリを生成することはなくなり、ビルド工程の段階で確実に失敗を検知できるようになりました。

テスト通過とアプリ組み上がりを同一視してはならない理由

Command Line Tools と Xcode の環境差異(Issue #10561)

今回のトラブルで発見が遅れた背景には、「単体テストが通っているからビルド環境も正常であり、アプリも問題なく動くはずだ」という思い込みがありました。

macOS 環境において、Command Line Tools(CLT)と Xcode の間には、開発者が意識しにくい環境差異が存在します。xcode-select -p が /Library/Developer/CommandLineTools を指している環境では、デフォルトの swift build は xcbuild の内部初期化エラー(Unknown error parsing property list)により即座に失敗します(SwiftPM Issue #10561)5。

このエラーを回避する手段として、ビルドシステムに --build-system native を指定すると、SwiftUI を使用しない単体テストは正常にビルド・実行され、すべて PASS します。

しかし、同じ CLT 環境で SwiftUI を含むアプリ本体に対して swift build -c release --build-system native を実行すると、cannot assign to property: 'self' is immutable というコンパイルエラーが発生して停止します。CLT 単体の環境では、SwiftUI のマクロプラグイン(SwiftUIMacros の StateMacro など)が展開されないためです。

現在、Swift プロジェクト全体として、Apple のビルドエンジンをベースとした新しい Swift Build への統合が進められており6、旧来の native ビルドシステムは非推奨化される方針です7。最新の言語機能やマクロを活用するアプリケーション本体のビルドには、Command Line Tools だけでなく Xcode 本体の環境が不可欠です。

将来的に Swift Build への統合が進めば、クロスプラットフォームや CLI 環境におけるビルド挙動の統一と環境差異の解消が期待されます。しかし過渡期にある現在においては、CI/CD やローカル環境で「どのツールチェーンが何を実行しているか」を前提条件として明示的に管理し、テストとアプリ本体の組み上がりを別々に検証するパイプライン設計が依然として不可欠です。

テストが保証するもの、保証しないもの

テストが合格したという事実は、アプリが組み上がって起動できることの証明にはなりません。

単体テストと、成果物のパッケージング・実起動検証の違いを整理すると以下のようになります。

比較項目単体テスト(swift test)アプリパッケージング・実起動
主たる検証対象コアロジック・アルゴリズム・関数の入出力バイナリ・Info.plist・リソースバンドルの統合
リソース参照の検証テスト対象が直接参照しない限り検証されないBundle.module が Contents/Resources/ を実際にロード
ビルド環境要件CLT + native 等で部分的に実行可能な場合があるXcode 本体(SwiftUIMacros 等のコンパイラプラグイン必須)
検出できる失敗ロジックの計算誤り、アサーション失敗バンドル未同梱、起動時クラッシュ、環境差異によるビルド失敗

検証対象と要求環境が異なるため、テスト通過とは別に、成果物の独立した検証ステップが必須となります。

単体テストの責務は、純粋なビジネスロジックや関数の正しさを確かめることです。一方で、リソースバンドルが正しいパスに同梱されているか、マクロが展開されてアプリ全体がコンパイルできるか、そして OS 上でプロセスが正常に起動できるかといった問題は、パッケージング後の独立した成果物検証ステップでなければ検出できません。

この環境制約とテストの限界は、のちにプロジェクトのエージェント向け開発ルールに明文化されました。

引用元: nqounet/saga コミット 6db937b:.agents/AGENTS.md

## Build
- `swift build` が `Unknown error parsing property list` で失敗したら、まず `xcode-select -p` と `ls /Applications | grep Xcode` を確認する。Command Line Tools のみの環境では、SwiftUI を含むアプリ本体はビルドできない(`--build-system native` は SwiftUIMacros が無く通らない)。キャッシュ削除やビルドシステム切替を試さず、Xcode が必要と報告する。
- テストが `--build-system native` で通っても、アプリ本体がビルドできる根拠にはならない(テストは SwiftUI を使わない)。

AI エージェントとの協業で守るべきフェイルファスト原則

エージェントも騙される「甘い if」をフェイルファストに改める

近年の開発現場では、Claude Code などの AI コーディングエージェントにビルドタスクやパッケージングスクリプトの作成を委譲するケースが増加しています。

しかし、AI エージェントにスクリプトを書かせると、筆者の実践環境(Claude Code 等)においても、処理の中断を避けて「気を利かせて if で囲む」コードを生成する傾向が観察されました。Google Shell Style Guide 8 でも規定されているように、エラー時は STDERR にメッセージを出力し、適切な非ゼロの終了ステータスで即座に処理を停止させることが原則です。

もしスクリプトが存在確認の if でエラーを黙って素通りさせていると、AI エージェントも exit 0 という終了ステータスを信じ込み、必須成果物が欠落していることに気づかずに「ビルド完了」と判断してしまいます。プロンプトで「注意深く確認してください」と念押しするだけでは不十分であり、スクリプト自体に決定論的なフェイルファスト停止機構(else exit 1)を埋め込んでおく必要があります。

環境制約を .agents/AGENTS.md にコード化する

また、前述した Command Line Tools と Xcode の環境差異のように、外部環境に依存する前提条件は、リポジトリ内の規約ファイル(.agents/AGENTS.md など)にコードとして明文化しておくことが極めて有効です。

前提条件がコード化されていないと、AI エージェントは swift build のエラーに遭遇した際、キャッシュ削除やビルドフラグの変更、無関係なファイルの書き換えといった無益な試行錯誤を繰り返し、コンテキストと時間を浪費してしまいます。前提環境と失敗時の停止ルールを明文化しておくことで、エージェントは無駄なリトライループに陥ることなく、適切な判断を下せるようになります。

よくある質問(FAQ)

Q. swift test が通っているのにアプリ起動時に Bundle.module でクラッシュするのはなぜですか?

A. 単体テストが SwiftUI やリソースに依存していない場合、リソースバンドルが欠落していてもテストは成功します。アプリ起動時に初めて Bundle.module が Contents/Resources/ を探索し、未同梱のためクラッシュします。

Q. シェルスクリプトに set -e を書いているのに if 文でスクリプトが止まらないのはなぜですか?

A. POSIX 仕様により、if や while に続くテスト式の実行中は set -e による停止が無効化されるためです。未検出時に即座に落とすには、else exit 1 を明示的に記述する必要があります。

Q. Command Line Tools だけで SwiftUI を含む macOS アプリを swift build できますか?

A. できません。SwiftUI のマクロ展開(SwiftUIMacros)には Xcode 本体が必要です。CLT 単体では Unknown error parsing property list やマクロ未展開によるコンパイルエラーが発生します。

まとめ

実務から得られた設計上の教訓は、以下の3点に集約されます。

  • 必須成果物の存在確認には必ず else exit 1 を置く: if [ -d ] 単体では未検出を素通りさせ、最も検出が困難なサイレント障害を引き起こします。
  • 単体テスト通過とアプリ組み上がりを同一視しない: ロジックの検証と成果物のパッケージング・起動検証は、要求環境も検証範囲も異なる独立した確認です。
  • 環境要件とフェイルファストをコード化する: AI エージェントとの協業では、プロンプト指示だけに頼るのではなく、スクリプト自体の決定論的停止と環境制約の明文化が品質を担保します。

ビルドパイプラインのフェイルファスト設計や、AI エージェントと協業する開発環境の標準化について、ぜひ コンタクトフォーム よりお気軽にご相談ください。

Footnotes

  1. Package Manager Resources (SE-0271) Anders Bertelrud, Ankit Aggarwal 著、2020年2月。§Resources Bundle(Foundation スタイルバンドルの導出規定)、§Runtime Access to Resources Bundle(Bundle.module アクセサ仕様)、§Declaring Resources(process と copy の規則)。 ↩

  2. Placing content in a bundle Apple Developer Documentation。macOS アプリケーションバンドルの標準構造(Contents/MacOS/, Contents/Resources/)およびリソースバンドル配置要件(※要約レベル参照)。 ↩

  3. POSIX Shell Command Language - 2.14 Special Built-In Utilities (set -e) The Open Group / IEEE Std 1003.1-2017。§2.14 set(if、while、until に続くテスト式における -e の明示的無視仕様)。 ↩

  4. BashFAQ/105: Why doesn’t set -e do what I expected? Greg’s Wiki (Wooledge.org)。条件分岐と set -e の組み合わせによるエラー素通り現象と、明示的エラーハンドリング(else exit 1)の必要性。 ↩

  5. swift build fails on initialization with Unknown error parsing property list · Issue #10561 halostatue 報告、2026年9月20日。Command Line Tools 単体環境における xcbuild のプロパティリスト解析エラーと --build-system native 回避策の報告。 ↩

  6. The Next Chapter in Swift Build Technologies Swift.org 公式ブログ、2025年2月1日。Swift Build オープンソース化と SwiftPM ビルドシステム統合の開始。 ↩

  7. SwiftPM on Swift Build - October Update Swift Forums、2025年10月27日。レガシー native ビルドシステムの非推奨化方針とロードマップ。 ↩

  8. Google Shell Style Guide Google Open Source Projects。§STDOUT vs STDERR(エラーの STDERR 出力規律)、§Checking Return Values(コマンド戻り値の確認と非ゼロ終了規律)。 ↩