HireVM エンジニアリング記録

クラウドMacでSwiftPMビルドツールプラグインの隔離環境を診断する

クラウドMacでSwiftPMビルドツールプラグインの隔離環境を診断する

同じSwiftパッケージでも、開発者のマシンでは正常にコードを生成できる一方、クラウドMacのクリーンなワークスペースでは Operation not permitted が発生したり、ビルドに成功しても古いファイルがコンパイルされたりすることがあります。多くの場合、これはマシン性能の問題ではありません。ビルドツールプラグインが宣言されていないファイルに依存している、誤ったディレクトリに書き込んでいる、またはローカルに残った以前の成果物によって問題が隠れていることが原因です。調査で重要なのは、キャッシュの削除を繰り返すことではなく、プラグインの入力、出力、実行境界を一つずつ再現して確認することです。

まず失敗しているレイヤーを特定する

SwiftPMビルドツールプラグインには3つのレイヤーが関係します。SwiftPMがコマンドの実行を計画し、プラグインの実行ファイルがコンテンツを生成し、Xcodeが生成済みファイルを使用します。まず、リポジトリのルートで完全なログを保存します。

set -o pipefail
rm -rf .ci
mkdir -p .ci
xcodebuild \
  -scheme App \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath "$PWD/.ci/DerivedData" \
  -clonedSourcePackagesDirPath "$PWD/.ci/SourcePackages" \
  build 2>&1 | tee .ci/build.log

grep -E 'sandbox|deny|plugin|Operation not permitted|No such file' .ci/build.log

ログにプラグインツール自体が見つからないと表示されている場合は、そのツールがパッケージターゲットのexecutable toolとして宣言されているか確認します。ツールの起動後に失敗している場合は、読み書きするパスを引き続き調べます。ビルドは成功しても生成内容が更新されない場合は、入出力の宣言を重点的に照合してください。

最初からサンドボックスを無効化して解決しようとしないでください。サンドボックスによる拒否は、生成ツールがビルドグラフの外部にあるコンテンツを読み取ったり書き換えたりしていることを、正確に示している場合がよくあります。制限を回避しても、問題を次回のクリーンビルドまで先送りするだけです。

入出力をビルドグラフに宣言する

堅牢なプラグインは、明示的に宣言された入力だけを読み取り、結果を pluginWorkDirectory に書き込みます。次の構成では、schemaと生成されたSwiftファイルの両方をSwiftPMに追跡させます。

import PackagePlugin

@main
struct CodegenPlugin: BuildToolPlugin {
    func createBuildCommands(
        context: PluginContext,
        target: Target
    ) async throws -> [Command] {
        let tool = try context.tool(named: "schema-gen")
        let input = target.directory.appending("Schemas/api.json")
        let output = context.pluginWorkDirectory
            .appending("Generated/API.swift")

        return [
            .buildCommand(
                displayName: "Generate API.swift",
                executable: tool.path,
                arguments: [input.string, output.string],
                inputFiles: [input],
                outputFiles: [output]
            )
        ]
    }
}

生成ツールは起動後に Generated の親ディレクトリを自ら作成し、渡された出力パスにだけ書き込む必要があります。リポジトリの Sources、デスクトップ、ユーザーのホームディレクトリ、固定された /Users/... などへ暗黙的に書き込んではいけません。ビルドツールプラグインは、今回のコンパイルに組み込まれる派生ファイルを生成するためのものです。リポジトリの内容を変更する必要がある処理は、ビルドのたびにソースコードを密かに書き換えるのではなく、開発者が明示的に実行するコマンドプラグインとして実装します。

暗黙的な依存関係を特定する

よくある暗黙的な入力には、現在の作業ディレクトリにある設定ファイル、環境変数が参照するテンプレート、ホームディレクトリ内のキャッシュ、生成ツールが自動走査するフォルダ全体などがあります。次の4項目を一つずつ確認してください。

確認項目 正しい方法 リスクを示す兆候
入力ファイル すべて inputFiles に含める 実行時に未宣言のディレクトリを再帰的に走査する
出力ファイル すべて outputFiles に含める 時刻やマシンによってファイル名が変わる
作業パス 引数で渡された絶対パスを使用する pwd またはホームディレクトリに依存する
生成順序 ソートして安定した順序で出力する ファイルシステムの列挙順に依存する

生成ツール自体の決定性を保つ

権限が正しくても、生成ツールが原因で増分ビルドが繰り返し無効になることがあります。最も一般的な原因は、ファイルのヘッダーに現在時刻、一時ディレクトリ、ホスト名などを書き込むことです。同じ入力から毎回異なるバイト列が生成されると、SwiftPMは実際に変更があったかどうかを判断できません。

クラウドMacでクリーンな生成処理を2回続けて実行し、ハッシュを比較します。

rm -rf .ci/first .ci/second
mkdir -p .ci/first .ci/second

.build/debug/schema-gen \
  Schemas/api.json .ci/first/API.swift

.build/debug/schema-gen \
  Schemas/api.json .ci/second/API.swift

shasum -a 256 .ci/first/API.swift .ci/second/API.swift
cmp .ci/first/API.swift .ci/second/API.swift

2つのハッシュは必ず一致しなければなりません。生成ツールではフィールド、ファイル、宣言もソートし、改行コードを統一し、内容が変わった場合にだけ対象ファイルを置き換える必要があります。最初に一時ファイルへ書き込み、比較後にアトミックに移動すれば、ビルドが中断した際に不完全なSwiftファイルが残ることを防げます。

クリーンなワークスペースでキャッシュ問題を再現する

ローカルで成功しても、宣言が完全であるとは限りません。ローカルの DerivedData、古いプラグイン出力、過去に手動生成したソースコードなどにより、宣言漏れが一時的に隠れることがあります。クラウドビルドでは、少なくとも一度は履歴状態のない環境で検証し、その後に2回目の増分ビルドを実行する必要があります。

2段階の検証方法

1回目は専用のビルドディレクトリを削除してからビルドし、すべての生成ファイルがプラグインによって作成されることを確認します。2回目は入力を変更せずに再度ビルドし、プラグインが理由なく出力を書き換えないことを確認します。続いてschemaのフィールドを1つだけ変更して3回目を実行し、対応する生成ファイルが実際に更新されることを確認します。

次のチェックを使うと、生成ファイルが誤った場所へ出力されている問題をすばやく見つけられます。

find "$PWD" -type f -name 'API.swift' -print
git status --short

正常な場合、派生ファイルはビルドディレクトリにあり、git status にプラグインが自動で書き換えたバージョン管理対象のソースコードは表示されません。2回目も処理が繰り返される場合は、出力ファイルが実際に存在するか、ツールがタイムスタンプを強制的に更新していないか、入力ディレクトリが広すぎる範囲で宣言されていないかを確認します。

クラウドMacの検証境界を固定する

HireVMのリモートビルド環境やその他のクリーンなmacOSノードでは、人による目視確認に頼らず、次のチェックをパイプラインへ組み込むことを推奨します。

  1. 使用するXcodeを固定し、ログの冒頭に xcodebuild -versionswift --version を記録します。
  2. ワークスペースごとに専用のDerivedDataとパッケージのチェックアウトディレクトリを設定し、並列ジョブによる書き込みの競合を防ぎます。
  3. 初回ビルドは空のディレクトリから開始し、プラグインがホームディレクトリ内の過去のファイルに依存していないことを検証します。
  4. プラグインが失敗する前後の詳細なビルドログを保存します。ただし、トークン、署名用データ、環境変数全体はログに出力しないでください。
  5. 重要な生成ファイルのハッシュを比較し、同じコミットから同じ結果を再現できることを確認します。
  6. 入力を1つ変更して再実行し、増分ビルドが想定した出力だけを更新することを確認します。
  7. コンソールで現在選択可能な構成を確認し、同時ビルド数に応じてリソースを選びます。プラグインの正しさを確認する処理が、特定のマシンに偶然残っている状態に依存してはいけません。

最終目標は、1回のビルドを「偶然成功」させることではなく、SwiftPMが生成手順を完全に理解できる状態にすることです。入力が変わったときは正確に再ビルドし、入力が変わらないときは余計な処理を行わず、新しいクラウドMacのワークスペースへ移行しても同じ成果物を生成できる必要があります。これを実現すれば、サンドボックスは障害ではなく、ビルド境界が実際に正しく完全であるかを検証する自動チェックとして機能します。

よくある質問

SwiftPMビルドツールプラグインからソースディレクトリを書き換えてよいですか?

推奨されません。生成物はプラグイン作業ディレクトリへ出力し、outputFilesで宣言します。リポジトリを書き換える処理は、利用者が明示的に実行するコマンドプラグインへ分離します。

ローカルでは成功するのにクラウドMac CIで拒否されるのはなぜですか?

ローカルの残存生成物が宣言漏れを隠しているか、生成ツールがホームディレクトリ、現在位置、絶対パスへ依存している可能性があります。クリーンな取得状態と固定パスで再現してください。

隔離機能を無効にすれば解決しますか?

恒久対策にはなりません。すべての入力と出力を宣言し、許可されたディレクトリだけへ書き込み、同じ入力から同一内容を生成できるよう修正します。

専有Apple Silicon物理ノード

次のリモートMacタスクを専有ノードで実行

タスクの規模に合わせて、販売中の2種類の構成、5つのノード、日額・週額・月額・四半期の利用期間から選べます。注文前に構成と米ドル金額をすべて確認できます。

構成を選んで注文