every Tech Blog

株式会社エブリーのTech Blogです。

Cursor × iOS開発 ビルドエラーをCursorに取り込む編

はじめに

こんにちは。開発部でiOSエンジニアをしている野口です。

以前にCursor✖️iOS開発の記事を書きまして、現在もCursorでiOS開発を続けているのですが、その記事では「ビルドエラーはXcodeで解消している」と記載していました。

実際に運用してみて、ビルドエラーが発生するたびにXcodeに切り替えて確認するのが少し手間に感じるようになってきました。そこで今回は、XcodeのビルドエラーをCursorに自動的に取り込んで、より快適にCursorで開発に集中できる環境を作ってみました。

この記事は上記の記事を前提にしている箇所がありますので、まだお読みでない方は先にそちらをご覧いただけるとわかりやすいかと思います。

今回やったこと

今回は、Xcodeのビルドエラーを自動的にCursorに取り込むスクリプト(xcode_build_watch.sh)を作成しました。

このスクリプトは以下の処理を自動化します。

  1. Xcodeでビルドを実行:AppleScriptでCmd+Rを送信
  2. ビルドログを監視fswatchで新しいログファイルの作成を検知
  3. エラーを解析:ログファイルからエラー行を抽出
  4. エラーを出力:エラーを出力(オプションでCursorのチャットに自動入力)

また、前回の記事でご紹介したtasks.jsonの設定を利用して、CursorでCmd+Rでスクリプトを実行できるようにします。

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "xcode.run",
            "type": "shell",
            "command": "${workspaceFolder}/xcode_build_watch.sh",
            "problemMatcher": []
        }
    ]
}

それでは、実際のスクリプトの詳細を見ていきましょう。

まず、スクリプト全体を掲載し、その後で各部分の動作を詳しく解説していきます。

xcode_build_watch.sh

#!/bin/bash

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 設定
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

# ビルドログディレクトリ
BUILD_LOG_DIR="/Users/<ユーザー名>/Library/Developer/Xcode/DerivedData/<プロジェクト名>-<ハッシュ>/Logs/Build"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
OUTPUT_FILE="$SCRIPT_DIR/latest_build_log.txt" # 最新のビルドログを保存するファイル
ERROR_TEMP_FILE="$SCRIPT_DIR/latest_errors.txt" # 最新のエラーを保存するファイル

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 関数定義
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

# Xcodeでビルドを実行
run_xcode_build() {
    echo "🚀 Xcodeビルド&監視を開始します"
    echo ""
    
    # Xcodeが起動しているか確認
    if ! pgrep -x "Xcode" > /dev/null; then
        echo "⚠️  警告: Xcodeが起動していません"
        echo "   Xcodeを起動してから再実行してください"
        return 1
    fi
    
    # XcodeでCmd+Rを実行
    if osascript \
        -e 'tell application "Xcode" to activate' \
        -e 'tell application "System Events" to keystroke "r" using {command down}' \
        -e 'delay 0.5' \
        -e 'tell application "Cursor" to activate' 2>/dev/null; then
        echo "✅ Xcodeでビルドを開始しました"
        echo ""
        return 0
    else
        echo "❌ エラー: Xcodeの操作に失敗しました"
        return 1
    fi
}

# ビルドログを監視
watch_build_logs() {
    echo "🔍 ビルドログ監視を開始しました"
    echo "⚡ 次のビルドが完了したら自動的にエラー解析を実行します"
    echo ""
    
    # ディレクトリの存在確認
    if [ ! -d "$BUILD_LOG_DIR" ]; then
        echo "❌ エラー: ビルドログディレクトリが見つかりません: $BUILD_LOG_DIR"
        return 1
    fi
    
    # fswatchのインストール確認
    if ! command -v fswatch &> /dev/null; then
        echo "❌ エラー: fswatchがインストールされていません"
        echo "   インストール: brew install fswatch"
        return 1
    fi
    
    # fswatchでビルドログディレクトリを監視(1回だけ)
    local event
    event=$(fswatch -1 -e ".*" -i "\\.xcactivitylog$" "$BUILD_LOG_DIR" 2>&1)
    local exit_code=$?
    
    # fswatchのエラーチェック
    if [ $exit_code -ne 0 ]; then
        echo "❌ エラー: fswatch実行エラー (終了コード: $exit_code)"
        echo "$event"
        return 1
    fi
    
    # eventが空でないことを確認
    if [ -z "$event" ]; then
        echo "❌ エラー: ビルドログの検出に失敗しました"
        return 1
    fi
    
    # ファイルが存在することを確認
    if [ ! -f "$event" ]; then
        echo "❌ エラー: 検出されたファイルが存在しません: $event"
        return 1
    fi
    
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo "🔔 新しいビルドログを検出しました"
    echo "📄 ファイル: $(basename "$event")"
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    echo ""
    
    # エラー解析を実行
    analyze_build_errors "$event"
}

# ビルドエラーを解析
analyze_build_errors() {
    local log_file="$1"
    
    echo "=== Xcodeビルドエラー解析 ==="
    echo ""
    
    # ログファイルが指定されていない場合は最新を検索
    if [ -z "$log_file" ]; then
        if [ ! -d "$BUILD_LOG_DIR" ]; then
            echo "❌ エラー: ビルドログディレクトリが見つかりません"
            echo "   パス: $BUILD_LOG_DIR"
            return 1
        fi
        
        log_file=$(ls -t "$BUILD_LOG_DIR"/*.xcactivitylog 2>/dev/null | head -1)
        
        if [ -z "$log_file" ]; then
            echo "❌ エラー: ビルドログが見つかりません"
            return 1
        fi
    else
        # ファイルの存在確認
        if [ ! -f "$log_file" ]; then
            echo "❌ エラー: 指定されたログファイルが存在しません"
            echo "   パス: $log_file"
            return 1
        fi
    fi
    
    echo "解析中: $(basename "$log_file")"
    echo ""
    
    # 必要なコマンドの確認
    if ! command -v gunzip &> /dev/null || ! command -v strings &> /dev/null; then
        echo "❌ エラー: 必要なコマンド(gunzip/strings)が見つかりません"
        return 1
    fi
    
    # ログを解凍
    if ! gunzip -c "$log_file" > "$OUTPUT_FILE" 2>/dev/null; then
        echo "❌ エラー: ログファイルの解凍に失敗しました"
        return 1
    fi
    
    # エラーを抽出
    local errors
    errors=$(strings "$OUTPUT_FILE" 2>/dev/null | \
        grep -oE '/Users[^"]+\.swift:[0-9]+:[0-9]+: error:[^"]*')
    
    if [ -z "$errors" ]; then
        echo "✅ エラーは見つかりませんでした"
        return 0
    fi
    
    echo "❌ ビルドエラーが見つかりました:"
    echo ""
    echo "$errors"
    echo ""
    
    # エラーを一時ファイルに保存
    echo "$errors" > "$ERROR_TEMP_FILE"
    
    # インタラクティブプロンプト(オプション)
    echo -n "Cursorチャットに送信しますか? [Y/n]: "
    read -r response
    
    case "$response" in
        [yY]|"")
            send_errors_to_cursor
            ;;
        [nN])
            echo "スキップしました"
            ;;
        *)
            echo "無効な入力です。スキップします。"
            ;;
    esac
    
    return 0  # エラーは見つかったが、処理は正常に完了
}

# Cursorチャットにエラーを送信(オプション)
send_errors_to_cursor() {
    if [ ! -f "$ERROR_TEMP_FILE" ]; then
        echo "❌ エラー: エラーファイルが見つかりません"
        echo "   パス: $ERROR_TEMP_FILE"
        return 1
    fi
    
    local errors
    errors=$(cat "$ERROR_TEMP_FILE")
    
    if [ -z "$errors" ]; then
        echo "❌ エラー: エラー内容が空です"
        return 1
    fi
    
    # エラー数をカウント
    local error_count
    error_count=$(echo "$errors" | wc -l | tr -d ' ')
    echo "📊 検出されたエラー数: $error_count"
    
    # エラーメッセージを整形してクリップボードにコピー
    local error_text="以下のエラーを修正してください:

$errors"
    
    if ! echo "$error_text" | pbcopy; then
        echo "❌ エラー: クリップボードへのコピーに失敗しました"
        return 1
    fi
    
    echo "📋 エラーをクリップボードにコピーしました"
    echo "💬 Cursorチャットを開いています..."
    
    # Cursorのチャットを開く
    if osascript \
        -e 'tell application "Cursor" to activate' \
        -e 'delay 0.3' \
        -e 'tell application "System Events" to keystroke "l" using {command down}' \
        -e 'delay 0.3' \
        -e 'tell application "System Events" to keystroke "v" using {command down}' 2>/dev/null; then
        
        # 通知を表示
        osascript -e 'display notification "Cursorチャットにエラーをペーストしました" with title "ビルドエラー"' 2>/dev/null
        
        echo "✅ 完了しました"
        return 0
    else
        echo "⚠️  警告: Cursorの操作に失敗しました(エラーはクリップボードにコピー済み)"
        return 1
    fi
}

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# メイン処理
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

main() {
    # Xcodeでビルド実行+監視
    run_xcode_build || exit 1
    watch_build_logs
    
    local exit_code=$?
    echo ""
    if [ $exit_code -eq 0 ]; then
        echo "✅ 処理が完了しました"
    else
        echo "⚠️  スクリプトがエラーで終了しました (終了コード: $exit_code)"
    fi
    
    return $exit_code
}

# スクリプト実行
main

解説

スクリプトの内容について、順を追って解説していきます。

事前準備

まず、このスクリプトを動作させるために必要な準備について説明します。

1. fswatchのインストール

ビルドログの監視にfswatchというツールを使用します。Homebrewでインストールできますので、以下のコマンドを実行してください。

brew install fswatch

fswatchはファイルシステムの変更を監視するツールで、今回は新しい.xcactivitylogファイルが作成されたタイミングを検知するために使用します。

2. ビルドログディレクトリのパスを設定

スクリプト内のBUILD_LOG_DIRのプレースホルダー(<ユーザー名><プロジェクト名><ハッシュ>)を、ご自身の環境に合わせて実際の値に置き換えてください。

BUILD_LOG_DIR="/Users/<ユーザー名>/Library/Developer/Xcode/DerivedData/<プロジェクト名>-<ハッシュ>/Logs/Build"

また、DerivedDataの場所は設定によって変わるので確認してください。

3. 実行権限を付与

スクリプトファイルに実行権限を付与します。

chmod +x xcode_build_watch.sh

これで準備は完了です!それでは、スクリプトがどのように動作するのか見ていきましょう。

全体の流れ

スクリプトのエントリーポイントはmain関数です。この関数が各ステップを順番に呼び出していきます。

main() {
    # Xcodeでビルド実行+監視
    run_xcode_build || exit 1
    watch_build_logs
    
    local exit_code=$?
    echo ""
    if [ $exit_code -eq 0 ]; then
        echo "✅ 処理が完了しました"
    else
        echo "⚠️  スクリプトがエラーで終了しました (終了コード: $exit_code)"
    fi
    
    return $exit_code
}

main関数では、まずrun_xcode_buildでXcodeのビルドを開始します。もしここで失敗したらexit 1でスクリプト全体を終了します。成功したら次のwatch_build_logsに進み、新しいビルドログの作成を待ち受けます。

このスクリプトは4つのステップで動作します。各関数が次の関数を順番に呼び出していく構造になっています。

1. Xcodeでビルド開始 (run_xcode_build)
   ↓
2. ビルドログを監視 (watch_build_logs)
   ↓  新しいログを検出したら
3. エラーを解析 (analyze_build_errors)
   ↓
4. エラーを出力
   ↓  オプション: ユーザーが承認したら、Cursorに入力 (send_errors_to_cursor)

それぞれのステップを詳しく見ていきましょう。

ステップ1: Xcodeでビルド開始

最初のステップでは、run_xcode_build関数でXcodeをアクティブにし、Cmd+Rでビルドを開始し、Cursorに戻ります。 なお、Cmd+Rのキーストロークが確実に実行されるよう、0.5秒の待機時間を設けてからCursorに戻るようにしています。

osascript \
    -e 'tell application "Xcode" to activate' \
    -e 'tell application "System Events" to keystroke "r" using {command down}' \
    -e 'delay 0.5' \
    -e 'tell application "Cursor" to activate'

各コマンドの説明:

  • tell application "Xcode" to activate:Xcodeをアクティブにする
  • keystroke "r" using {command down}:Cmd+R(ビルド実行)を送信
  • delay 0.5:0.5秒待機
  • tell application "Cursor" to activate:Cursorに戻る

この処理により、Xcodeに切り替えることなくビルドが開始され、すぐにCursorでの作業に戻れます。

ステップ2: ビルドログを監視

次に、watch_build_logs関数fswatchを使って新しいビルドログの作成を待ち受けます。

fswatch -1 -e ".*" -i "\\.xcactivitylog$" "$BUILD_LOG_DIR"

各オプションの説明:

  • -1:1回だけイベントを検知したら終了
  • -e ".*":いったんすべてのファイルを除外(デフォルトではすべての変更を検知してしまうため)
  • -i "\\.xcactivitylog$":その上で、.xcactivitylogで終わるファイルだけを監視対象に含める
  • $BUILD_LOG_DIR:ビルドログが保存されるディレクトリ

Buildディレクトリ内で.xcactivitylogファイルだけを監視するための設定をしています。

Xcodeでビルドが完了すると、以下の場所に新しい.xcactivitylogファイルが作成されます。

/Users/<ユーザー名>/Library/Developer/Xcode/DerivedData/<プロジェクト名>-<ハッシュ>/Logs/Build/*.xcactivitylog

このファイルの作成を検知して、次のステップに進みます。

ステップ3: エラーを解析

3つ目のステップでは、analyze_build_errors関数がビルドログからエラーを抽出します。

3-1. ログを解凍

.xcactivitylogファイルはgzip形式で圧縮されているため、まず解凍します。

gunzip -c "$log_file" > "$OUTPUT_FILE"

3-2. エラーを抽出

解凍したログから、エラー行だけを抽出する処理を行います。

解凍したログは以下のようなイメージになっています。(加工して、抜粋しています)

from project 'Feature')f06c6d4c1b47c741^705ce8521b47c741^1(2@2#32"com.apple.dt.IDE.BuildLogSection38"Compile 376 Swift source files (arm64)70"CompileSwift normal arm64 (in target 'Feature' from project 'Feature')0e86764c1b47c741^158fe3521b47c741^-42213"/Users/username/Projects/MyApp/Sources/MyFile.swift:96:27: error: expected expression in list of expressions type: ,
/Users/username/Projects/MyApp/Sources/MyFile.swift:61:17: error: expected expression after 'await'
strings "$OUTPUT_FILE" | \
    grep -oE '/Users[^"]+\.swift:[0-9]+:[0-9]+: error:[^"]*'

各コマンドの役割:

  1. strings:バイナリから読み取り可能な文字列を抽出
  2. grep -oE '/Users[^"]+\.swift:[0-9]+:[0-9]+: error:[^"]*':拡張正規表現を使ってエラーメッセージを抽出
    • -o:マッチした部分だけを出力
    • -E:拡張正規表現を使用
    • /Users[^"]+/Usersから始まり、"以外のすべての文字が1文字以上続く(ファイルパスを抽出)
    • \.swift:[0-9]+:[0-9]+.swiftファイル、行番号、列番号
    • : error:[^"]*: error:に続く、"以外のすべての文字(エラーメッセージ本文を抽出)

この処理により、以下のような形式でエラーが抽出されます。

/Users/username/Projects/MyApp/Sources/MyFile.swift:96:27: error: expected expression in list of expressions type: ,
/Users/username/Projects/MyApp/Sources/MyFile.swift:61:17: error: expected expression after 'await'

ステップ4: エラーを出力

4つ目のステップでは、analyze_build_errors関数が抽出したエラーをターミナルに出力します。

echo "❌ ビルドエラーが見つかりました:"
echo ""
echo "$errors"

基本的な機能はここまでです。エラーがターミナルに表示されるので、それを確認してコードを修正できます。

オプション: Cursorのチャットに自動入力

さらに便利にするために、エラーをCursorのチャットに自動入力する機能も用意しています。この機能はオプションなので、スクリプトから削除しても問題ありません。

まず、ユーザーに確認プロンプトを表示します。

echo -n "Cursorチャットに送信しますか? [Y/n]: "
read -r response

Yまたはエンターキーを押すと、次の処理が実行されます。

まず、エラーメッセージを整形してクリップボードにコピーします。

echo "以下のエラーを修正してください:

$errors" | pbcopy

次に、Cursorのチャットを開いてエラーを貼り付けます。

osascript \
    -e 'tell application "Cursor" to activate' \
    -e 'delay 0.3' \
    -e 'tell application "System Events" to keystroke "l" using {command down}' \
    -e 'delay 0.3' \
    -e 'tell application "System Events" to keystroke "v" using {command down}'

各コマンドの説明:

  • tell application "Cursor" to activate:Cursorをアクティブにする
  • keystroke "l" using {command down}:Cmd+Lでチャットを開く
  • keystroke "v" using {command down}:Cmd+Vでコピーしたエラーを貼り付け

これで、エラーが自動的にCursorのチャットに入力され、すぐにAIに質問できる状態になります!

まとめ

今回は、XcodeのビルドエラーをCursorに自動的に取り込むスクリプトをご紹介しました。

前回の記事と組み合わせることで、CursorでのiOS開発環境がさらに充実したものになったと感じています。

少し設定は必要ですが、一度設定してしまえば快適に開発できるようになりますので、ぜひお試しください!