クラウド Mac CI のメタデータ整理と成果物検査

クラウド Mac CI のメタデータ整理と成果物検査

クラウド Mac でのビルドが成功し、圧縮ファイルも問題なく展開できたとしても、受け取り側には ._Config.json や .DS_Store が見えたり、アップロード時に出所不明の拡張属性が検出されたりすることがあります。原因は通常コンパイラではなく、Finder での操作、異なるファイルシステム間でのコピー、アーカイブツールによる macOS メタデータの扱いの違いにあります。対策としてワークスペースを一度だけ乱暴に消去するのではなく、配布工程を監査可能なステージング、スキャン、アーカイブ、再検証の4段階に分けます。

配布パッケージに含めてよいものを先に定義する

macOS のファイルには、名前と内容だけでなく、拡張属性、リソースフォーク、Finder メタデータが付随することがあります。ローカルファイルシステムでは問題にならなくても、ZIP、共有ドライブ、オブジェクトストレージへ移すと AppleDouble サイドカーファイルに変換される場合があります。

パッケージ化しながら削除するのではなく、最初に成果物のルールを定義します。

対象 デフォルトポリシー 理由
.DS_Store 拒否 Finder のディレクトリ表示だけを記録するため
._* 拒否 リソースフォークを異なるファイルシステム間で変換した際に生成されることが多いため
com.apple.quarantine 検査して個別に処理 ダウンロードツールやブラウザによって付与された可能性があるため
com.apple.ResourceFork 成果物の種類に応じて判断 通常の設定パッケージでは不要だが、特定のファイルが依存している場合があるため
署名済みアプリケーションバンドル 構造を保持して再検証 一律に削除すると署名異常のリスクが高まるため

クリーンアップの対象は、ソースコードのワークスペースではなく独立したステージングディレクトリにします。これにより、ポリシーに誤りがあっても、キャッシュ、依存関係ディレクトリ、次回のビルドに必要な入力を変更せずに済みます。

配布物に署名済みアプリケーションが含まれる場合は、最初に元の成果物の署名を検証します。通常の文書、設定ファイル、ログには、より厳格なメタデータ拒否ポリシーを適用できます。この2種類の対象に同じ再帰削除コマンドを使用しないでください。

使い捨てのステージングディレクトリを作成する

ステージングディレクトリは現在のジョブ専用とし、終了時に削除する必要があります。固定パスの /tmp/release を使い回すと、並列ジョブが互いの内容を上書きする可能性があります。

#!/bin/bash
set -euo pipefail

SOURCE="${1:?source path required}"
OUTPUT="${2:?output path required}"
STAGE="$(mktemp -d "${TMPDIR:-/tmp}/bamini-artifact.XXXXXX")"
UNPACK="$(mktemp -d "${TMPDIR:-/tmp}/bamini-unpack.XXXXXX")"

cleanup() {
  rm -rf "$STAGE" "$UNPACK"
}
trap cleanup EXIT

/usr/bin/ditto --norsrc "$SOURCE" "$STAGE/payload"
find "$STAGE/payload" -type f -name '.DS_Store' -delete
find "$STAGE/payload" -type f -name '._*' -delete

mktemp によりジョブごとに異なるディレクトリが作成され、trap によって成功、失敗、中断のいずれの場合もクリーンアップが実行されます。ここでは ditto --norsrc で内容をコピーし、リソースフォークを意図的に引き継がないようにしています。ただし、入力ディレクトリに実体化された ._ ファイルが最初から含まれている可能性があるため、後続のスキャンは省略しません。

ワークフローで完全なアプリケーションバンドルを配布する必要がある場合は、まず、そのアプリケーションに必要な内容がコピー方法によって保持されることを確認してください。より安全なのは、アプリケーションバンドルと通常の添付ファイルを別々にステージングし、それぞれに適したポリシーを適用する方法です。

拡張属性を一律削除せずスキャンする

xattr -cr は便利ですが、対象範囲が広すぎます。既知の隔離属性だけでなく、まだ検査ルールに組み込まれていない他の属性も削除します。その結果、パイプラインは「成功」しても、根本原因が隠されてしまいます。

最初に属性名を出力し、明示的に禁止した項目が見つかった場合にジョブを停止できます。

ATTR_REPORT="$STAGE/xattr.txt"

if /usr/bin/xattr -lr "$STAGE/payload" >"$ATTR_REPORT" 2>&1; then
  if grep -E 'com\.apple\.(quarantine|ResourceFork)' "$ATTR_REPORT"; then
    echo "disallowed macOS metadata found" >&2
    exit 41
  fi
fi

if find "$STAGE/payload" -type f \( -name '.DS_Store' -o -name '._*' \) -print -quit |
  grep -q .; then
  echo "hidden metadata files found" >&2
  exit 42
fi

このゲートでは「検出した時点で失敗」とし、メタデータを持ち込んだ工程を先に特定できるようにしています。発生源を確認した後、ダウンロード、コピー、生成の各工程で個別に修正します。業務上、特定の拡張属性を許可する必要がある場合は、ディレクトリ全体を無条件で許可するのではなく、対象ファイルの範囲と属性名を許可リストに記載してください。

メタデータが追加された工程を記録する

ソースコードのチェックアウト後、コンパイル後、ステージング後に、それぞれスキャンを1回実行します。異常が最初に現れた段階が、調査対象の境界になります。一般的な発生源には、GUI でのディレクトリ閲覧、非ネイティブファイルシステムからのコピー、ツールによるダウンロード後の展開などがあります。最終アーカイブの直前だけにスキャンしても汚染は阻止できますが、原因となった工程は特定できません。

アーカイブ後に再度展開して検査する

ステージングディレクトリがクリーンでも、アーカイブツールがメタデータを再び書き込まないとは限りません。ZIP の生成後にメンバー名を検査し、さらに新しいディレクトリへ展開して再検証します。

rm -f "$OUTPUT"
/usr/bin/ditto -c -k --norsrc --keepParent \
  "$STAGE/payload" "$OUTPUT"

/usr/bin/unzip -Z1 "$OUTPUT" >"$STAGE/members.txt"

if grep -E '(^|/)\.DS_Store$|(^|/)\._[^/]+$' "$STAGE/members.txt"; then
  echo "archive contains forbidden metadata files" >&2
  exit 43
fi

/usr/bin/ditto -x -k "$OUTPUT" "$UNPACK"

if find "$UNPACK" -type f \( -name '.DS_Store' -o -name '._*' \) -print -quit |
  grep -q .; then
  echo "extracted artifact failed metadata check" >&2
  exit 44
fi

メンバー一覧の検査は高速で、最初のゲートに適しています。展開後の再検証では、パス変換や展開処理の挙動も確認できます。アップロードシステムが実際に受け取るのはアーカイブファイルであり、利用者が最終的に操作するのは展開後の内容であるため、両方を実行する必要があります。

パッケージに署名済みアプリケーションが含まれる場合は、展開後の .app を特定し、厳格な署名検証をもう一度実行します。パッケージ化前のコピーだけを検証すると、アーカイブオプションによる変更を検出できません。

ゲートを再現可能な配布フローに組み込む

スクリプトの引数は「入力ディレクトリ」と「出力ファイル」の2つに固定し、各ジョブで次の証跡を生成することを推奨します。

  1. ステージング前の成果物のパスと種類。
  2. 拡張属性のスキャン結果。
  3. ZIP のメンバー一覧。
  4. 展開後の隠しファイル検査結果。
  5. アプリケーションバンドルが存在する場合の再署名検証結果。
  6. 最終アーカイブの SHA-256 ダイジェスト。

ダイジェストは shasum -a 256 artifact.zip で生成できます。2つの ZIP の内容が意味的に完全一致することまでは証明できませんが、アップロード、ダウンロード、引き渡しの途中でバイト単位の変更が発生していないことは確認できます。

BAMini のクラウド Mac でこの種のジョブを実行する場合は、まずコンソールで現在選択可能な構成を確認し、スクリプトをリポジトリに追加して CI から呼び出してください。特定のメンバーが手動でクリーンアップすることに依存してはいけません。ゲートが失敗した場合はメンバー一覧と属性レポートを保存し、プロジェクトの内容を含む一時ディレクトリは削除します。前回の残留物が判定に影響しないよう、次のジョブでは必ず新しいステージングディレクトリを作成してください。

最終的な目標は、すべての拡張属性を消去することではなく、チームが明示的に承認した内容だけを配布パッケージに含めることです。ステージングの分離によって変更範囲を制御し、段階ごとのスキャンによって発生源を特定し、展開後の再検証によって実際の配布結果を確認します。この3つをすべて実施することで、コピー経路が変わるたびに隠しメタデータが再発するのを防げます。

よくある質問

ワークスペース全体に xattr -cr を実行してもよいですか?

推奨しません。必要なリソースフォークまで削除し、上流工程の問題を隠す可能性があります。専用ステージング上で許可リストに基づき削除してください。

.DS_Store を削除しても ._ ファイルが残るのはなぜですか?

._ ファイルは AppleDouble のサイドカーファイルで、.DS_Store とは別物です。圧縮時にリソースフォークを除外し、生成後のメンバー一覧も検査します。

署名済みアプリはいつ検証すべきですか?

元のアプリを先に検証してからステージングへコピーします。無差別な属性削除は避け、圧縮後に別ディレクトリへ展開して再度厳密に署名を検証します。

専用物理ノード

次のビルドをクラウド Mac mini で実行

構成、請求期間、ノードのリージョンを選び、専用物理マシンで開発・ビルド・自動化タスクを実行できます。

Mac miniを今すぐレンタル