同じCIスクリプトが対話型ターミナルでは正常に動作するのに、無人運用のクラウドMacでは誤ったSchemeを選択する場合、原因はXcode自体ではなく、スクリプトが「人間向けのテキスト」をインターフェースとして扱っていることにある場合がほとんどです。システム言語、ツールのバージョン、警告が挿入される位置、さらには余分な空白ひとつだけでも、grep、cut、固定行番号に依存した処理は破綻します。より確実な方法は、JSONまたはplistを優先して読み取り、終了コード、未加工の出力、解析結果をそれぞれ別に保存することです。
不安定なテキスト解析を特定する
まず、パイプライン内にあるリスクの高い組み合わせを検索します。
grep -RInE 'xcodebuild|simctl|system_profiler|sw_vers' ci scripts \
| grep -E 'grep|awk|sed|cut|tail|head'
すべてのパイプ処理が誤りというわけではありません。危険なのは、「Build Settings」のような自然言語を検索する、コロン以降の2列目を切り出す、対象デバイスが必ず3行目にあると仮定するといった処理です。LC_ALL=C を指定すれば言語差は抑えられますが、フィールドの順序、警告内容、将来のバージョンでの出力形式までは固定できません。
コマンド出力が主に人間の閲覧を目的としているなら、それを安定した機械向けインターフェースだと決めつけないでください。
監査時には、各読み取り処理について3つの点を明確にします。必要なフィールドは何か、コマンドに構造化形式が用意されているか、フィールドが欠けている場合は失敗させるのか、それとも明示的なデフォルト値を使うのか、という点です。空文字列を後続のアーカイブ処理へそのまま渡してはいけません。
ビルド設定をJSON契約に変換する
xcodebuild -showBuildSettings -json を使うと、対象のビルド設定を直接取得できます。最初にコマンドの終了コードを確認してからファイルを解析し、標準エラーをJSONに混在させないようにします。
set -euo pipefail
mkdir -p artifacts/environment
if ! xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-showBuildSettings \
-json \
> artifacts/environment/build-settings.json \
2> artifacts/environment/build-settings.stderr; then
cat artifacts/environment/build-settings.stderr >&2
exit 1
fi
python3 - <<'PY'
import json
from pathlib import Path
path = Path("artifacts/environment/build-settings.json")
if not path.exists() or path.stat().st_size == 0:
raise SystemExit("build settings output is empty")
items = json.loads(path.read_text(encoding="utf-8"))
if len(items) != 1:
raise SystemExit(f"expected one target, got {len(items)}")
settings = items[0].get("buildSettings", {})
required = ["PRODUCT_BUNDLE_IDENTIFIER", "SDKROOT", "CONFIGURATION_BUILD_DIR"]
missing = [key for key in required if not settings.get(key)]
if missing:
raise SystemExit("missing build settings: " + ", ".join(missing))
print(settings["PRODUCT_BUNDLE_IDENTIFIER"])
PY
ワークスペースに複数のターゲットが含まれる場合は、配列の要素数が1であることを強制してはいけません。target フィールドで対象を選択し、一致するものがない場合や複数ある場合は処理を終了させます。これにより、拡張機能のターゲットを追加しても、スクリプトが誤った設定を気付かずに読み取ることを防げます。
simctlのJSONでデバイス選択を固定する
シミュレータ一覧は、特に行単位の解析に適していません。デバイス名には空白が含まれることがあり、同名のデバイスが異なるランタイムに存在する場合もあります。まず結果全体を保存し、その後でランタイム、名前、利用可能状態を条件に絞り込みます。
xcrun simctl list -j devices available \
> artifacts/environment/simulators.json
python3 - <<'PY'
import json
from pathlib import Path
data = json.loads(
Path("artifacts/environment/simulators.json").read_text(encoding="utf-8")
)
matches = []
for runtime, devices in data.get("devices", {}).items():
if not runtime.startswith("com.apple.CoreSimulator.SimRuntime.iOS-"):
continue
for device in devices:
if device.get("isAvailable") and device.get("name") == "iPhone 16":
matches.append((runtime, device["udid"]))
if len(matches) != 1:
raise SystemExit(f"expected one matching simulator, got {len(matches)}")
print(matches[0][1])
PY
本番用スクリプトでは、「最新」を自動選択するのではなく、想定するランタイムのバージョンをリポジトリの設定に記録しておく必要があります。最新バージョンを自動的に選ぶと、システムコンポーネントの更新によってテストの基準環境が直接変わってしまいます。最終的にテストを実行する際は、デバイス名だけを指定するよりも、UDIDを渡すほうが曖昧さを抑えられます。
空集合を見落とさない
「一致するデバイスがない」状態は、明確な失敗として扱う必要があります。シミュレータを一時作成する前に、必要なランタイムが実際に存在することを確認してください。タスク終了後は、そのタスクが作成したデバイスだけを削除し、共有デバイスセットを消去してはいけません。並列タスクでは、それぞれに別の -derivedDataPath と独立したCoreSimulatorデバイスセットを指定するのが理想です。
plistからホスト情報を読み取る
macOSのバージョン情報はシステムplistから直接読み取れるため、sw_vers のラベルテキストを解析する必要はありません。
SYSTEM_PLIST=/System/Library/CoreServices/SystemVersion.plist
product_version=$(
/usr/libexec/PlistBuddy -c 'Print :ProductVersion' "$SYSTEM_PLIST"
)
build_version=$(
/usr/libexec/PlistBuddy -c 'Print :ProductBuildVersion' "$SYSTEM_PLIST"
)
printf '%s\n' "$product_version" > artifacts/environment/macos-version.txt
printf '%s\n' "$build_version" > artifacts/environment/macos-build.txt
xcodebuild -version > artifacts/environment/xcode-version.txt
プロジェクトのplistを読み取るときは、plutil -extract KEY raw -o - FILE を優先して使用します。フィールドが存在しない可能性がある場合は、先に plutil -type KEY FILE で確認し、欠落を許容するかどうかをスクリプト側で判断します。XMLを正規表現で処理してはいけません。plistはバイナリ形式の場合があり、文字列がエスケープされている可能性もあります。
診断可能なデータ収集境界を設ける
構造化形式だからといって、コマンドが必ず成功するとは限りません。信頼できる収集処理では、少なくとも4種類の証拠を保持する必要があります。コマンドの終了コード、標準エラー、変更していない未加工ファイル、そして解析後の簡潔な結果です。未加工ファイルはツールの挙動を後から検証するために使い、簡潔な結果は後続タスクで利用します。
各収集処理は1つの対象だけを担当し、出力を一時ファイルへ書き込む構成を推奨します。構文と必須フィールドをすべて検証してから、mv で正式なファイルをアトミックに置き換えます。これにより、コマンドが中断されても、不完全なJSONが残りません。
コミット前のチェック項目
パイプラインの変更をマージする前に、少なくとも次の事項を確認します。
- スクリプトが固定行番号や自然言語のラベルを使ってフィールドを抽出していない。
- 標準出力と標準エラーが別々のファイルに書き込まれている。
- JSONとplistが読み取り前に構文検証されている。
- 配列の絞り込みで、一致件数がゼロの場合と複数の場合の両方が明確に処理されている。
- Xcode、macOSのビルド番号、選択したデバイスのUDIDをタスクとともにアーカイブできる。
- ログには環境情報だけを記録し、トークン、認証情報、完全な環境変数を出力しない。
- ツールのバージョン更新後は、先に収集処理の受け入れテストを実行し、その後で本番ビルドタスクを有効にする。
最終目標は、すべてのコマンドをJSONに置き換えることではありません。機械は構造化データを利用し、人間は診断ログを読むという明確な境界を設けることです。構造化インターフェースが存在しない場合でも、コマンドのバージョンを固定し、言語環境を分離し、列数を検証したうえで、解析失敗を失敗として扱う必要があります。一見妥当に見える空値をそのまま使い続けてはいけません。
よくある質問
LC_ALL=Cを設定すれば十分ですか?
十分ではありません。言語差は減らせますが、項目順、空白、警告文、ツール更新による形式変更は固定できません。自動処理にはJSONかplistを使います。
構造化コマンドの失敗時に何を保存すべきですか?
終了コード、標準エラー、元のJSONまたはplist、Xcodeのバージョン、macOSのビルド番号を保存し、解析前にファイルが空でなく構文も正しいことを確認します。
次の開発タスクをクラウドMacで進める
Berry M4またはBerry M4 Proを選び、5つのノード、利用期間、追加オプションを確認して注文します。