クラウドMacでSwiftPM依存関係を固定し再現可能にする

CI/CD ·約 11 分

クラウドMacでSwiftPM依存関係を固定し再現可能にする

クラウドMacでSwiftPM依存関係を固定し再現可能にする

同じコミットが開発用Macでは正常にビルドできるのに、別のクラウドMacでは異なる依存関係が解決される場合、原因は通常、コンパイラの不規則な不具合ではありません。解決ファイル、Xcodeのバージョン、キャッシュの境界が一体として固定されていないことにあります。短期間だけ利用するノードでは、特にこの問題が表面化しやすくなります。提供時のディレクトリは新規でも、パイプラインのスクリプトが古いキャッシュを暗黙に再利用したり、ビルド中に依存関係のバージョンを書き換えたりする可能性があるためです。

同一ビルドの条件を先に定義する

再現可能性とは、毎回ワークディレクトリ全体を保持することではありません。同じ入力から、同じ依存関係の集合が得られることを意味します。最低でも、コードのコミット、Package.resolvedの内容、Xcodeのビルドバージョン、プロセッサアーキテクチャの4項目を記録してください。Xcodeのメジャーバージョンだけでは不十分です。同じメジャーバージョンでも、ツールチェーンのパッチレベルが異なる場合があります。

まず、ノード上で環境のスナップショットを保存します。

set -euo pipefail
xcode-select -p
xcodebuild -version
uname -m
swift --version
git rev-parse HEAD

アプリケーションプロジェクトでは、通常Package.resolvedをリポジトリにコミットします。ワークスペースで一般的なパスはApp.xcworkspace/xcshareddata/swiftpm/Package.resolvedです。プロジェクトファイルしかない場合は、プロジェクト内部のワークスペースディレクトリに置かれることもあります。解決ファイルを2つ同時に管理せず、パイプラインが実際にどちらを読み込んでいるかを先に確認してください。

判定基準は明確です。ビルドスクリプトの実行後にgit status --porcelainで解決ファイルの変更が表示されるなら、そのビルドを元のコミットに対する検証結果とは見なせません。

バージョン範囲ではなく解決結果を固定する

Package.swiftのバージョン範囲は「選択可能なバージョン」を表し、Package.resolvedは「今回実際に選択されたバージョン」を記録します。ノードの初回セットアップ時には明示的に依存関係を解決できますが、本番ビルドでは、すでに記録されているバージョンだけを受け入れるようにします。

set -euo pipefail
ROOT="$PWD"
CACHE="$ROOT/.build-cache/swiftpm"

xcodebuild \
  -resolvePackageDependencies \
  -workspace App.xcworkspace \
  -scheme App \
  -clonedSourcePackagesDirPath "$CACHE"

git diff --exit-code -- App.xcworkspace/xcshareddata/swiftpm/Package.resolved

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -destination 'generic/platform=macOS' \
  -clonedSourcePackagesDirPath "$CACHE" \
  -onlyUsePackageVersionsFromResolvedFile \
  build

ターゲットがiOSの場合は、destinationをパイプラインで定義済みの汎用デバイスまたはシミュレータターゲットに変更してください。現在起動しているデバイスをスクリプトに自動選択させてはいけません。純粋なSwiftパッケージではswift package resolveswift buildを使用できますが、プロジェクトとワークスペースは一貫してxcodebuildを使用し、2つの解決経路によってディレクトリの状態が分岐しないようにします。

入力のダイジェストでキャッシュ境界を分ける

キャッシュの目的は、ダウンロードとチェックアウトにかかる時間を短縮することだけです。依存関係のバージョンを決める情報源にしてはいけません。安全なキャッシュキーには、アーキテクチャ、Xcodeのビルド情報、解決ファイルのダイジェストを含めます。

set -euo pipefail
RESOLVED="App.xcworkspace/xcshareddata/swiftpm/Package.resolved"
ARCH="$(uname -m)"
XCODE="$(xcodebuild -version | tr '
' '-' | tr ' ' '_')"
LOCK_HASH="$(shasum -a 256 "$RESOLVED" | awk '{print $1}')"
CACHE_KEY="${ARCH}-${XCODE}-${LOCK_HASH}"
printf '%s
' "$CACHE_KEY"
入力の変更 作業キャッシュを再利用できるか 理由
アプリケーションのソースコードのみ変更 できる 依存関係の集合は変わらない
Package.resolvedを変更 できない チェックアウトするバージョンまたはリビジョンが変わっている
Xcodeのビルドバージョンを変更 できない ツールチェーンや成果物の形式が変わる可能性がある
arm64と別のアーキテクチャを切り替え できない バイナリ成果物を混在させられない

複数ノードで実行する場合、複数のマシンから同じディレクトリへ同時に書き込ませてはいけません。キーごとにアーカイブした読み取り専用キャッシュを保存し、各物理ノードでローカルの作業ディレクトリへコピーする方式を採用できます。失敗したジョブを再試行するときも、過去のアーカイブをすべて削除するのではなく、現在の作業コピーだけを先に消去します。

依存関係のドリフトを検出したら即座に失敗させる

ビルドの前後でリポジトリの状態を確認すれば、解決ファイルの書き換えと、スクリプトの生成物が誤ってソースディレクトリに出力された問題を同時に検出できます。本番ジョブは、クリーンなチェックアウトから開始してください。

set -euo pipefail
test -z "$(git status --porcelain)"
BEFORE="$(shasum -a 256 App.xcworkspace/xcshareddata/swiftpm/Package.resolved)"
xcodebuild -workspace App.xcworkspace -scheme App \
  -clonedSourcePackagesDirPath "$PWD/.build-cache/swiftpm" \
  -onlyUsePackageVersionsFromResolvedFile build
AFTER="$(shasum -a 256 App.xcworkspace/xcshareddata/swiftpm/Package.resolved)"
test "$BEFORE" = "$AFTER"

失敗直後にバージョン制約を解除して再解決してはいけません。それでは設定ミスが成功したかのように見えてしまいます。解決ログ、環境スナップショット、キャッシュキーを保存したうえで、リポジトリにロック済みの解決結果がないのか、依存関係のリビジョンが無効になったのか、あるいはノードが誤ったXcodeを選択したのかを判断するのが正しい手順です。

よくある失敗を順番に切り分ける

解決処理がタイムアウトする

まず、ノードから依存関係の取得元へアクセスできるかを確認し、次にディスクの空き容量とキャッシュディレクトリの権限を確認します。ネットワークが復旧したら、同じ解決ファイルを使用して再試行してください。先に依存関係をアップグレードしてはいけません。毎回同じパッケージで失敗する場合は、その依存関係の宣言とリビジョンが現在も一致しているかを個別に検証します。

解決には成功するがコンパイルに失敗する

まず、2台のノードでxcode-select -pxcodebuild -versionuname -mの出力を比較します。バイナリ依存関係がある場合は、現在のアーキテクチャに対応するスライスが含まれていること、またプロジェクトで設定された最小システムバージョンを満たしていることも確認してください。該当するキャッシュキーのローカルコピーを削除して再ビルドすれば、キャッシュ破損とソースコードの非互換性を切り分けられます。

コミット前のチェックリスト

  1. リポジトリ内で実際に有効なPackage.resolvedは1つだけである。
  2. ビルドコマンドで-onlyUsePackageVersionsFromResolvedFileが有効になっている。
  3. キャッシュキーにアーキテクチャ、Xcodeのビルド情報、解決ファイルのダイジェストが含まれている。
  4. 各ノードが個別の書き込み可能な作業ディレクトリを使用している。
  5. ビルド前後で解決ファイルのダイジェストが一致している。
  6. ログにはコミットID、キャッシュキー、ツールチェーンのバージョンを残し、認証情報は記録しない。

よくある質問

Package.resolvedはリポジトリへコミットすべきですか?

アプリケーションやデプロイ対象のサービスでは通常コミットし、記録済みバージョンだけを使ってビルドします。ライブラリは別方針でも構いませんが、公開前にクリーンな環境で対応範囲を検証します。

複数のクラウドMacで同じSwiftPMキャッシュへ書き込めますか?

同じディレクトリへの同時書き込みは避けます。CPUアーキテクチャ、Xcodeビルド番号、Package.resolvedのハッシュからキーを作り、ノードごとに作業コピーを用意します。

依存解決後にコンパイルだけ失敗する場合は何を確認しますか?

選択中のXcodeパスとビルド番号を確認し、該当キーのキャッシュを作り直します。その後、バイナリ依存の対応アーキテクチャ、最小システム要件、解析ファイルの変更を調べます。

専用物理ノード

再現可能な環境を同じクラウドMacに保持する

固定のモデル、リージョン、契約期間を選び、完全なmacOSグラフィカルインターフェースとコマンドラインで開発や自動化タスクを実行できます。

クラウドMacを今すぐレンタル