クラウドMacの成果物をdittoで安全に梱包する方法

CI/CD ·約 12 分

クラウドMacの成果物をdittoで安全に梱包する方法

クラウドMacでのビルドは正常に完了したのに、成果物を納品先のマシンへダウンロードすると、スクリプトが実行できない、シンボリックリンクが通常ファイルに変わる、アプリケーションバンドルの検証に失敗するといった問題が発生することがあります。こうした問題の原因はコンパイル工程ではなく、最後の圧縮・展開処理にある場合が少なくありません。macOSのファイルには、内容やパスだけでなく、拡張属性、アクセス制御リスト、リソースフォーク、シンボリックリンクの関係も含まれることがあります。納品フローで「アーカイブを開けること」しか確認していないと、これらの変化がリリース工程まで持ち込まれてしまいます。

保持すべき成果物を先に定義する

ワークスペース全体をそのまま圧縮してはいけません。まず納品対象だけを含むステージングディレクトリを作成し、どのメタデータを成果物の要件に含めるかを明確にします。

一般的な対象には、アプリケーションバンドル、フレームワーク、動的ライブラリ、コマンドラインツール、設定テンプレート、シンボルファイルがあります。キャッシュディレクトリ、テスト用の一時ファイル、バージョン管理ディレクトリ、ローカルログは含めるべきではありません。ステージングディレクトリを使えば、梱包中にビルドタスクがソースディレクトリを変更することも防げます。

set -euo pipefail

ROOT="$PWD"
SOURCE="$ROOT/build/Release"
STAGE="$ROOT/artifacts/stage"
ARCHIVE="$ROOT/artifacts/release.zip"

rm -rf "$STAGE"
mkdir -p "$STAGE"
ditto "$SOURCE/MyProduct.app" "$STAGE/MyProduct.app"
ditto "$SOURCE/MyFramework.framework" "$STAGE/MyFramework.framework"

最初の確認では、通常ファイル、ディレクトリ、シンボリックリンクの数を記録します。特にシンボリックリンクは、ツール間で受け渡す際にリンク先へ展開されやすく、アーカイブ容量の変化や相対パスの破損につながります。

find "$STAGE" -type f | wc -l
find "$STAGE" -type d | wc -l
find "$STAGE" -type l -print

梱包前にmacOSのメタデータを確認する

ls -le@ でパーミッション、ACL、拡張属性を確認し、xattr で重要な対象の属性内容を展開します。見慣れない属性をすべて不要なデータと判断してはいけません。

ls -le@ "$STAGE"
xattr -lr "$STAGE" > "$ROOT/artifacts/xattr-before.txt"
find "$STAGE" -type l -exec sh -c '
  for item do
    printf "%s -> %s\n" "$item" "$(readlink "$item")"
  done
' sh {} + > "$ROOT/artifacts/symlinks-before.txt"

xattr -cr を万能な修復手段として使ってはいけません。再帰的に属性を削除すると、一部の表面的なエラーは一時的に解消しても、同時に調査に必要な情報まで失われます。正しい手順は、まず属性を洗い出して出所を判断し、不要だと確認できた個別の属性だけを最小限の範囲で処理することです。

実行ファイルのパーミッションビットも確認する必要があります。リポジトリ内でスクリプトに実行権限が付いていても、ステージングディレクトリへコピーした後も維持されているとは限りません。find "$STAGE" -type f -perm -111 で基準を記録できます。特定のスクリプトに実行権限が必須なら、人の記憶に頼るのではなく、受け入れテスト用スクリプトに要件を組み込みます。

dittoで納品用アーカイブを作成する

ditto はmacOSに標準搭載されているツールで、アプリケーションバンドルやフレームワークディレクトリの処理に適しています。ZIP作成時にリソースと拡張属性を保持し、ステージングディレクトリ自体も含めることで、受け取り側に一貫したトップレベル構造を提供できます。

rm -f "$ARCHIVE"
ditto -c -k \
  --sequesterRsrc \
  --keepParent \
  "$STAGE" \
  "$ARCHIVE"

shasum -a 256 "$ARCHIVE" > "$ARCHIVE.sha256"
du -h "$ARCHIVE"

アーカイブのファイル名と内部のトップレベルディレクトリ名は固定します。タイムスタンプ、マシン名、一時ワークスペースの絶対パスをディレクトリ名に含めると、後続スクリプトがエントリーポイントを推測しなければなりません。パイプラインで一意の識別子が必要な場合は、アーカイブ内部の構成を変更するのではなく、コミット番号を外部の成果物レコードに保存します。

出所を管理できないファイルについては、ステージングディレクトリへ追加する前に、ファイル名に改行、不可視文字、異常なUnicodeの組み合わせが含まれていないかも確認します。通常の空白は安全に扱えますが、Shellによるパスの分割を防ぐため、スクリプトでは常に変数を二重引用符で囲む必要があります。

独立したディレクトリへ展開して比較する

受け入れテストでは元のディレクトリに上書き展開してはいけません。欠落したファイルが古いファイルによって見えなくなる可能性があるためです。毎回新しいディレクトリを使用し、ditto で展開します。

VERIFY="$ROOT/artifacts/verify"

rm -rf "$VERIFY"
mkdir -p "$VERIFY"
ditto -x -k "$ARCHIVE" "$VERIFY"

test -d "$VERIFY/stage/MyProduct.app"
test -d "$VERIFY/stage/MyFramework.framework"

find "$VERIFY/stage" -type l -exec sh -c '
  for item do
    printf "%s -> %s\n" "$item" "$(readlink "$item")"
  done
' sh {} + > "$ROOT/artifacts/symlinks-after.txt"

diff -u \
  "$ROOT/artifacts/symlinks-before.txt" \
  "$ROOT/artifacts/symlinks-after.txt"

ファイル内容の検証では、梱包前と展開後のディレクトリについて、それぞれ一覧を生成します。パスに空白が含まれる可能性があるため、引用符で保護されていないループは使用しません。次のPythonコードは相対パスで並べ替え、通常ファイルのSHA-256を計算します。

from hashlib import sha256
from pathlib import Path
import sys

root = Path(sys.argv[1])

for path in sorted(root.rglob("*"), key=lambda p: str(p.relative_to(root))):
    if path.is_file() and not path.is_symlink():
        digest = sha256(path.read_bytes()).hexdigest()
        print(digest, path.relative_to(root))

stage と verify/stage に対してそれぞれ実行し、出力を diff で比較します。これにより、内容の変化、ファイルの欠落、意図しない追加をまとめて検出できます。

構造と署名の検証をリリースゲートにする

成果物に署名済みのアプリケーションバンドルやフレームワークが含まれる場合は、展開後に改めて署名を検証します。問題がアーカイブツールによって生じる可能性があるため、圧縮前に一度だけ検証しても不十分です。

codesign --verify --deep --strict \
  "$VERIFY/stage/MyProduct.app"

codesign -d --verbose=4 \
  "$VERIFY/stage/MyProduct.app" 2>&1 |
  sed -n '1,20p'

--deep は最終的な整合性チェックに適していますが、階層ごとの原因特定には代用できません。失敗した場合は、まず原因がメインアプリケーション、組み込みフレームワーク、補助実行ファイルのどこにあるかを特定し、対象パスに対して codesign --verify --strict --verbose=4 を実行します。同時に梱包前後の xattr 出力を比較し、変化がアーカイブ工程で発生したものかを確認します。

受け入れテストの失敗条件は、トップレベルディレクトリの不一致、ファイル一覧の差異、シンボリックリンク先の変化、必須実行ビットの欠落、重要な拡張属性の異常、署名検証の失敗として固定することを推奨します。いずれか一つでも発生した場合は、再圧縮して偶然の成功を期待するのではなく、リリースを停止すべきです。

よくある失敗と納品チェックリスト

最も一般的な失敗は、複数のツールを混在させることです。たとえば、最初にスクリプトで通常のZIPを作成し、その後アップロードプログラムが自動的に展開して再圧縮するケースがあります。処理経路が長くなるほど、メタデータの意味を追跡しにくくなります。梱包ツールは一つに統一し、元のアーカイブをそのまま成果物ストレージへ渡します。

二つ目の失敗は、ビルドと梱包を並行して進めることです。コンパイラがファイルを書き込んでいる間に処理すると、ファイル一覧とアーカイブが異なる時点の状態を反映する可能性があります。正しい方法は、ビルドプロセスが正常終了するまで待ち、結果を読み取り専用のステージングディレクトリへコピーしてからアーカイブを開始することです。

三つ目の失敗は、同じマシン上だけでテストすることです。少なくとも古い検証用ディレクトリを削除し、改めて展開したうえで、アーカイブ内のパスから検証を開始する必要があります。可能であれば、独立したタスクでも同じスクリプトを実行します。BerryVM上で長期運用するパイプラインでは、メタデータの確認結果、アーカイブのハッシュ、受け入れテストのログを同一バッチの納品記録として保存できます。ただし、ログに認証情報やマスキングされていない環境変数を含めてはいけません。

最終チェックリストは簡潔に保ちます。ステージングディレクトリが固定済みであること、シンボリックリンクを記録済みであること、拡張属性を確認済みであること、アーカイブのハッシュを生成済みであること、独立した展開が完了していること、ファイル一覧が一致していること、実行ビットが要件どおりであること、必要な署名検証に合格していることを確認します。これらを実施すれば、梱包はビルド後に片手間で行う作業ではなく、繰り返し実行でき、問題発生時に確実に失敗させられるエンジニアリング上のリリースゲートになります。

よくある質問

macOSの成果物をzip -rだけで圧縮すると何が問題ですか?

一般的なZIP処理では、リソースフォーク、拡張属性、ACL、シンボリックリンクの意味が失われる場合があります。アプリやフレームワークにはdittoが適しています。

梱包前にxattr -crを実行してもよいですか?

ディレクトリ全体への一括実行は推奨しません。まず属性を一覧化して用途を確認し、不要と判断できた項目だけを限定的に処理します。

アーカイブのSHA-256が一致すれば検証は完了ですか?

完了ではありません。ハッシュはアーカイブ自体が変化していないことしか示しません。別の場所へ展開し、構造、リンク、属性、必要な場合はコード署名も確認します。

専用の物理Mac

次の開発タスクをクラウドMacで進める

Berry M4またはBerry M4 Proを選び、5つのノード、利用期間、追加オプションを確認して注文します。

構成を選んで注文