エンジニアリング実践

クラウドMac CIで作るiOS OSSライセンス検査

クラウドMac CIで作るiOS OSSライセンス検査

依存関係を更新した後、チームは問題なくアーカイブを完了したものの、新しいコンポーネントがOSSライセンス一覧に含まれていないことにリリース直前で気づきました。多くの場合、問題はビルドそのものではなく、依存関係の取得元が分散していることにあります。SwiftPMにはロックファイルがあり、CocoaPodsには別のディレクトリ構成があります。さらに、リポジトリへソースコードを直接コピーしたり、バイナリパッケージを追加したりしているケースもあります。検査をクラウドMac CIで実行すれば、リリース担当者が直前にディレクトリを手作業で調べるのではなく、固定されたワークスペースで同じプロジェクトから一覧を生成し、ポリシーと照合して証跡を保存できます。

ゲートで確認すべき項目を先に定義する

実際に運用できるゲートでは、少なくとも4つの問いに答える必要があります。今回のビルドで実際に解決されたコンポーネントは何か、そのバージョンまたはコミットは何か、ライセンス全文はどこにあるか、その条件は承認済みか、という点です。ファイル名を検索しただけで通過させてはいけません。LICENSE が存在していても、内容が変更されていないとは限らず、そのコンポーネントが現在のターゲットへ実際にリンクされているとも限らないためです。

結果は、機械処理用のマニフェストと人が確認するレポートに分けることを推奨します。機械処理用のマニフェストにはJSONを使用し、コンポーネント名、取得元、固定バージョン、ライセンス種別、テキストのハッシュ、検出パスを記録します。人向けのレポートにはMarkdownを使用し、追加、削除、変更された項目を示します。どちらもパイプラインの成果物として保存します。

ゲートが検査するのは、依存関係の実態が既定のポリシーと一致しているかどうかです。未知の項目を検出した場合は、ライセンス名から処理方法を自動的に推測するのではなく、リリースを停止して再確認を依頼する必要があります。

ビルド結果から実際の依存関係を棚卸しする

まずXcodeで依存関係の解決を完了させてから、ワークスペースが実際に使用しているディレクトリを読み取ります。これにより、開発者のマシンに残っている古いキャッシュを誤ってスキャンすることを防げます。各ジョブでは個別のDerivedDataを使用し、ジョブの終了後にクリーンアップします。

set -euo pipefail

ROOT="$(pwd)"
OUT="$ROOT/build/license-audit"
DD="$ROOT/build/DerivedData"
mkdir -p "$OUT"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -resolvePackageDependencies \
  -derivedDataPath "$DD"

find "$DD/SourcePackages/checkouts" \
  -maxdepth 3 \
  -type f \
  \( -iname 'license*' -o -iname 'copying*' -o -iname 'notice*' \) \
  -print0 | sort -z > "$OUT/swiftpm-license-files.bin"

find "$ROOT/Pods" \
  -maxdepth 4 \
  -type f \
  \( -iname 'license*' -o -iname 'copying*' -o -iname 'notice*' \) \
  -print0 | sort -z > "$OUT/pods-license-files.bin"

バイナリ依存関係は、ファイル拡張子だけで判断できません。ダウンロードURLに対応する内部コンポーネントID、チェックサム、バージョン、パッケージに同梱された告知文も併せて記録する必要があります。リポジトリ内へコピーしたソースコードについては、compliance/manual-components.json を管理し、各ディレクトリの所有者を明記します。これにより、スキャンのたびに曖昧なマッチングへ依存せずに済みます。

ロックファイルも必ず同時に記録する

Package.resolved、Podfile.lock、手動管理コンポーネントの一覧をレポートディレクトリへコピーします。ゲートでは、一覧内の各コンポーネントがいずれかの固定済み取得元に存在することを検証し、逆方向にも、ロックファイル内の直接依存関係がレポートから消えていないことを確認する必要があります。プロジェクトに複数のワークスペースが含まれる場合は、メインアプリだけでなく、それぞれについて依存関係を解決します。

ライセンスを正規化してフィンガープリントを計算する

ライセンス本文は、改行コードや行末の空白だけが異なることも少なくありません。まずUTF-8とLF改行に統一し、行末の空白を削除してからSHA-256を計算できます。ただし、本文の段落、著作権表記、年は削除しないでください。削除すると、実際の変更が見えなくなります。

python3 - <<'PY'
from pathlib import Path
import hashlib
import json

root = Path("build/license-audit/licenses")
rows = []
for path in sorted(root.rglob("*")):
    if not path.is_file():
        continue
    text = path.read_text(encoding="utf-8", errors="strict")
    normalized = "\n".join(line.rstrip() for line in text.splitlines()).strip() + "\n"
    digest = hashlib.sha256(normalized.encode("utf-8")).hexdigest()
    rows.append({"path": str(path), "sha256": digest})
Path("build/license-audit/license-hashes.json").write_text(
    json.dumps(rows, ensure_ascii=False, indent=2) + "\n",
    encoding="utf-8"
)
PY

ライセンス種別の識別結果とテキストのフィンガープリントは、分けて保存します。種別はポリシーとの照合に使用し、フィンガープリントはアップストリームによる気づきにくい変更の検出に使用します。確実に識別できない場合は UNKNOWN とし、ファイル名が見慣れたものであっても自動分類してはいけません。

ポリシーファイルで変更を制御する

ポリシーファイルはバージョン管理の対象とし、明確なコードレビュープロセスを経て変更する必要があります。ライセンス識別子ごとに allow、review、deny を設定し、特定のコンポーネントについて承認済みテキストのハッシュを記録できます。検査結果には、次の判定基準を推奨します。

状況 CI結果 次の対応
既知のコンポーネントで、バージョンと本文に変更がない 通過 レポートを保存
新しいバージョンで、ライセンスのフィンガープリントに変更がない 要確認 コンポーネントの変更を確認
新しいコンポーネントまたは未知のライセンス ブロック 取得元と承認を追加
既存コンポーネントのライセンス本文が変更された ブロック 全文の差分を比較
ロックファイルにはコンポーネントがあるが、レポートにない ブロック 検出ルールを修正

許可リストには、広範なライセンス種別だけを記載すべきではありません。コンポーネントには追加のNOTICE、デュアルライセンス、個別のリソースライセンスが付随する場合があるため、レポートには関連するすべてのファイルを残す必要があります。バイナリパッケージについては、現在のダウンロード物のチェックサムが承認記録と一致することも検証します。

レポートをマージとリリースのフローへ組み込む

通常のマージリクエストでは、依存関係の解決、差分検査、レポート生成を実行できます。リリースジョブでは、さらにアーカイブ内容を照合し、最終成果物が同じロックファイルに対応していることを確認します。ベースラインは compliance/license-baseline.json に置き、CIでは端末の色付き出力ではなく、構造化されたフィールドだけを比較することを推奨します。

ゲートが失敗した場合、ログにはコンポーネント、以前の値、新しい値、証跡のパスを直接表示し、単に「ライセンス検査に失敗しました」とだけ返す状態を避けます。レポートディレクトリには、少なくともロックファイルのコピー、正規化したテキスト、ハッシュ一覧、ポリシーのバージョン、差分の要約を含めます。ArmCLI上の並列ジョブでは、それぞれ独立した作業ディレクトリを使用し、あるジョブによる依存関係の更新が別のジョブのスキャン結果へ影響しないようにします。

リリース前のチェック項目

リリース前に、依存関係の解決で未記録のローカルパスが使用されていないこと、すべての直接依存関係と推移的依存関係にバージョンの取得元があること、未知のライセンス数がゼロであること、手動管理コンポーネント一覧に記載されたディレクトリが現在も存在すること、レポートとアーカイブが同じコミットを使用していること、CI成果物にアクセストークン、非公開のダウンロードURL、マスキングされていない環境変数が含まれていないことを確認します。

このプロセスの価値は、単に告知ページを1枚生成することではありません。依存関係の変更を、レビュー可能なエンジニアリング上のイベントとして扱える点にあります。ロックファイル、ライセンスのフィンガープリント、ポリシーのベースラインが同期していれば、アーカイブ完了後にコンポーネントの取得元を調べ直すのではなく、依存関係がリリースブランチへ入った時点で問題を検出できます。

よくある質問

Package.resolvedだけを確認すれば十分ですか?

十分ではありません。固定されたSwiftPMのバージョンは分かりますが、ライセンス本文の有無、CocoaPods、コピーされたソース、バイナリ依存関係は別途確認が必要です。

どの変更でリリースを停止すべきですか?

ライセンスが見つからない新規依存、許可リスト外の条件、ロックファイルとの不一致、既存ライセンス本文の変更があれば、手動確認まで停止します。

ArmCLI クラウドMac

次のビルドに備えて専用物理Macを用意

タスクに合わせて機種、データセンター、利用期間を選択できます。ノードは365日継続稼働しており、実際の利用可能状況はコンソールのリアルタイム情報をご確認ください。