同じXcodeビルドジョブを数日間連続で実行していると、DerivedDataへの書き込み、依存関係キャッシュの更新、古い成果物の削除などで、突然 Permission denied が発生することがあります。リポジトリをチェックアウトし直せば一時的に復旧しても、次の並列ジョブで再発する場合があります。このとき、最初からXcodeを疑ったり、ディレクトリの権限を安易に広げたりしてはいけません。多くの場合、いずれかのスクリプトが別の実行ユーザーに切り替わった、異なる umask を継承した、または作業領域に余分なACLを残したことが原因です。
まず失敗時の状態を保存する
権限問題では、「クリーンアップして再実行」することで失敗時の状態が失われるのが最も厄介です。まずジョブの実行ユーザー、所属グループ、ホームディレクトリ、カレントディレクトリ、デフォルトの権限マスクを記録します。その後、最初に失敗したパスを調べます。最初からディスク全体を走査する必要はありません。
printf 'user=%s
' "$(id -un)"
printf 'groups=%s
' "$(id -Gn)"
printf 'home=%s
' "$HOME"
printf 'cwd=%s
' "$PWD"
printf 'umask=%s
' "$(umask)"
target="${FAILED_PATH:?set FAILED_PATH first}"
stat -f 'owner=%Su group=%Sg mode=%Sp path=%N' "$target"
ls -led "$target"
stat では所有者と基本モードを確認でき、ls -le ではACLを確認できます。ディレクトリのモード上は書き込み可能に見えても、実際にアクセスできるとは限りません。親ディレクトリに実行権限がない、ACLに制限エントリがある、ファイルが別のジョブユーザーに属している、といった理由で処理が妨げられることがあります。
最初に失敗したパスと、その親ディレクトリだけを順に調べてください。作業領域全体の権限を再帰的に変更すると、調査に必要な状態が失われるだけでなく、鍵、キャッシュ、ビルド成果物が無関係なプロセスに公開されるおそれもあります。
3種類の権限ずれを切り分ける
所有者が変わっている
最も多いのは、いずれかのステップが権限昇格コマンドを使って依存関係をインストールしたり、ファイルをコピーしたりした結果、生成物が別のユーザーの所有物として残るケースです。まず、現在のジョブユーザーが所有していない項目を一覧表示します。
workspace="${WORKSPACE:?set WORKSPACE first}"
find "$workspace" -x ! -user "$(id -un)" -print
出力が特定のジョブディレクトリに集中している場合は、再帰的な chown で原因を覆い隠すのではなく、そのディレクトリを作成したスクリプトを確認してください。CIの作業領域は、作成時点からジョブユーザーが所有する状態にするのが理想です。高い権限での操作が必要な場合でも、出力先をリポジトリ、キャッシュ、DerivedDataにしないでください。
想定外のACLがある
Finderでの操作、移行スクリプト、コピー用ツールによってACLが保持されることがあります。ls -le の基本権限行の下に番号付きのエントリが表示された場合は、そのACLがどこから追加されたのかを確認してください。再作成可能な一時作業領域であることが確実な場合に限り、対象を限定してACLを削除できます。
job_dir="${JOB_DIR:?set JOB_DIR first}"
chmod -RN "$job_dir"
このコマンドをユーザーのホームディレクトリや認証情報のディレクトリに対して実行してはいけません。修正後にもう一度 ls -led を実行し、ACLが消えており、基本モードも要件どおりであることを確認します。
umaskが一致していない
SSHの対話セッション、CIデーモン、単独で動くスクリプトが、同じShell設定を読み込むとは限りません。あるジョブが 077 でキャッシュを作成すると、後続ジョブが同じグループに属していても再利用できない場合があります。起動ファイルに依存せず、ジョブのエントリーポイントで明示的に指定してください。
umask 022
install -d -m 0755 "$JOB_DIR"
install -d -m 0755 "$JOB_DIR/DerivedData"
install -d -m 0755 "$JOB_DIR/Artifacts"
機密データには専用ディレクトリと、より厳格なモードを使用してください。ビルドキャッシュを共有するために、すべての権限を一律に緩めてはいけません。
ジョブごとに独立した作業領域を使う
並列ジョブが同じDerivedDataやアーカイブディレクトリを共有すると、あるジョブのクリーンアップ処理が、別のジョブによる書き込みと衝突する可能性があります。リポジトリID、コミットID、ジョブ番号を組み合わせてディレクトリを作成し、そのパスをビルドコマンドへ明示的に渡すことを推奨します。
run_id="${CI_RUN_ID:?set CI_RUN_ID first}"
root="$HOME/ci-runs/$run_id"
src="$root/source"
derived="$root/DerivedData"
artifacts="$root/Artifacts"
umask 022
install -d -m 0755 "$src" "$derived" "$artifacts"
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-derivedDataPath "$derived" \
archive \
-archivePath "$artifacts/App.xcarchive"
共有キャッシュとジョブの出力は分離してください。依存関係のダウンロードキャッシュは読み取り専用で再利用できますが、DerivedData、アーカイブ、ログはジョブ単位で隔離します。これにより権限の相互汚染を減らせるだけでなく、ジョブが終了するまで失敗時の状態を保つこともできます。
| パスの種類 | 推奨される所有単位 | ライフサイクル | 並列書き込みの可否 |
|---|---|---|---|
| ソースコードのチェックアウト | 単一ジョブ | 1回の実行 | 不可 |
| DerivedData | 単一ジョブ | 1回の実行 | 不可 |
| アーカイブとログ | 単一ジョブ | 検収後に削除 | 不可 |
| ダウンロードキャッシュ | 固定のジョブユーザー | ジョブをまたいで保持 | キャッシュ処理のみ更新可 |
ビルド前後に検証ゲートを設ける
権限チェックは、問題発生後に手作業で行うのではなく、ジョブのエントリーポイントに組み込むべきです。ビルド前に、ディレクトリへ書き込めること、所有者が正しいこと、想定外のACLがないことを確認します。ビルド後には、異なる所有者のファイルが生成されていないかを確認します。次のチェックは、権限ずれを検出すると即座にジョブを終了します。
test -d "$JOB_DIR"
test -w "$JOB_DIR"
unexpected_owner="$(
find "$JOB_DIR" -x ! -user "$(id -un)" -print -quit
)"
if [ -n "$unexpected_owner" ]; then
printf 'unexpected owner: %s
' "$unexpected_owner" >&2
exit 1
fi
if ls -led "$JOB_DIR" | tail -n +2 | grep -q '^[[:space:]]*[0-9]:'; then
printf 'unexpected ACL on job directory
' >&2
exit 1
fi
チームでキャッシュを共有する必要がある場合は、キャッシュへ書き込む主体、更新タイミング、アトミックに置き換える方法を個別に定義してください。すべてのビルドジョブから同じディレクトリを同時に変更してはいけません。ジョブ完了後のクリーンアップでも、今回の run_id に対応するパスだけを削除し、その前に対象パスが想定したルートディレクトリの配下にあることを検証してください。
修正をスクリプトの発生源に反映する
信頼できる修正では、4つの問いに答えられる必要があります。どのステップが異常なファイルを作成したのか、作成時にどのユーザーを使用したのか、どの umask を継承したのか、なぜ共有ディレクトリへ書き込んだのか、という点です。原因を特定したら、手動ログイン後の環境に依存するのではなく、ディレクトリの作成、実行ユーザー、出力先をスクリプトに明記してください。
RunnerVMのクラウドMacでCIを実行する場合も、ジョブの開始時に最小限の環境スナップショットを出力し、現在選択できる構成をコンソールで確認してください。マシンの再起動やジョブの移行後でも、エントリースクリプトがディレクトリと権限の基準状態を再構築できれば、ビルドは前回の実行で残された状態に依存しません。最終的な目標は、あらゆる権限制限をなくすことではなく、各ファイルについて、作成者、読み書きできる範囲、クリーンアップの責任を予測可能にすることです。
よくある質問
Permission deniedが出たら作業領域全体にchmod 777を実行してよいですか?
推奨できません。所有者やACLの問題を隠し、無関係なプロセスにも書き込みを許します。statとls -leで失敗したパスを確認し、対象ジョブの領域だけを修正します。
同じスクリプトがSSHでは成功し、CIジョブでは失敗するのはなぜですか?
実行ユーザー、HOME、umask、PATH、起動環境が異なる可能性があります。ジョブ開始時に値を記録し、必要な権限基準をスクリプト内で明示してください。
次のビルドを専用の物理Macで実行
利用期間に合わせてクラウドMacノードを選び、コンソールから注文、接続情報、サポート依頼を管理できます。