# Mac なしで Flutter アプリを iOS に出す — Codemagic で署名を通すまで ([English](flutter-ios-codemagic.en.md)) Mac を持たずに Flutter アプリを App Store Connect へ上げ、TestFlight で実機に入れるまでの記録。 **署名だけで7回失敗した。** どれも「エラーメッセージが指しているものと、実際に足りないものが違う」型で、 メッセージを読んで対処すると外し続ける。同じ所で止まっている人向けに、原因と順番を残す。 証明書も Provisioning Profile も Codemagic に作らせる。**Xcode からの書き出しは1回も使っていない。** --- ## 失敗の一覧と、実際の原因 | 出たエラー | 実際に足りなかったもの | | --- | --- | | `No development certificates available` | Runner ターゲットに署名の3設定が無い | | `Runner requires a provisioning profile` | **秘密鍵(`CERTIFICATE_PRIVATE_KEY`)の未登録** | | `No matching profiles found` | プロファイルを作る前に使おうとしていた | | `Path .../export_options.plist does not exist` | 書き出し先が2つのコマンドで食い違っていた | **最後まで効いていた真因は1つ** — `CERTIFICATE_PRIVATE_KEY` の未登録だった。 **証明書は秘密鍵から作るものなので、鍵が無ければどのコマンドを足しても作れない。** 答えはログに出ていた(`Cannot save Signing Certificates without certificate private key` / `Provisioning Profiles: []`)。**設定ファイルだけ見てログを読まなかったため、2回診断を外した。** **メッセージではなくログを読む。** --- ## 準備(画面での操作) ### 1. Apple Developer Program に加入する - **Organization は D&B 上の事業形態が法人でないと通らない。** D-U-N-S 番号は認識されるが「組織が法人として登録されていません」で弾かれる。 個人事業主は Individual で加入することになる - **Individual だと App Store の表示名が本名で固定される。** 屋号を出せるのは Organization で、かつ**最初のアプリを登録するときだけ** - 支払いから有効化まで**約2時間**だった。電話確認は入らなかった ### 2. Bundle ID を登録する `developer.apple.com` → Certificates, Identifiers & Profiles → Identifiers → + - **Apple は bundle ID に `_`(アンダースコア)を許さない**(英数字・ハイフン・ピリオドのみ)。 Android の applicationId をそのまま持ち込めないことがある。 **揃わないなら「揃えない」と決めて記録に残す**(次に見た人が不具合と誤認するため) - **App Store Connect でアプリを作る前にここで登録する。** 登録していないとアプリ作成画面のプルダウンに出てこない - Capabilities のチェックは触らない(Firebase も広告もここでの追加設定は要らない) ### 3. App Store Connect でアプリを作る `appstoreconnect.apple.com` → マイApp → + → 新規App - ここで **App Store ID(10桁)**が発行される。「アプリ情報」のページで確認できる - **ビルドは要らない。** 名前と置き場所を予約するだけ - アプリ名はストアに出る名前そのもので、検索にも効く。 App Store には Google Play に無い**サブタイトル(30文字)**の枠がある ### 4. App Store Connect API key を作る `appstoreconnect.apple.com` → ユーザとアクセス → 統合 → App Store Connect API - **Issuer ID / Key ID / `.p8` ファイルの3つ**が要る - **`.p8` は1回しかダウンロードできない。** リポジトリの外へ保管する - Codemagic の画面で**アプリごとに名前を付けて登録する**(`codemagic.yaml` から名前で参照する) ### 5. 証明書用の秘密鍵を作る **Mac は要らない。Windows の Git Bash で動く。** ``` ssh-keygen -t rsa -b 2048 -m PEM -f ios-cert-private-key -q -N "" ``` - **この鍵が無いと証明書を作れない。** 証明書は秘密鍵から作るもの - **リポジトリの外へ保管する** - **失うと証明書の作り直しになる。Apple の配布証明書は3本が上限**なので枠を1つ消費する ### 6. Codemagic に環境変数を登録する アプリの設定 → Environment variables。**Secret にチェックを入れる。** | 変数名 | 値 | | --- | --- | | `CERTIFICATE_PRIVATE_KEY` | 手順5の鍵の全文(`-----BEGIN` から `-----END` まで) | | `GOOGLE_SERVICE_INFO_PLIST` | `GoogleService-Info.plist` を base64 にした文字列(Firebase を使う場合) | `.gitignore` で除外しているファイルはリポジトリ経由で渡せないので、環境変数から書き戻す。 ``` base64 -w0 ios/Runner/GoogleService-Info.plist ``` **手順4の API キーはここではない。** あちらは Codemagic の統合機能に登録するもので、 `codemagic.yaml` の `integrations.app_store_connect` から名前で参照する。 **【落とし穴】変数には group 名を付ける。** Codemagic は group 経由でしかビルドへ変数を渡さない。 **group 無しで登録すると、ビルドからは空のまま届く。エラーにならないので気づけない。** `codemagic.yaml` の `environment.groups` に同じ group 名を並べること。 --- ## codemagic.yaml — 外すと落ちるところ ### 署名は3段階に分ける ```yaml - name: Set up signing script: | app-store-connect fetch-signing-files "$BUNDLE_ID" \ --type IOS_APP_STORE \ --create keychain initialize keychain add-certificates xcode-project use-profiles \ --project ios/Runner.xcodeproj \ --export-options-plist $CM_BUILD_DIR/export_options.plist ``` - **`--create` を落とさない。** 初回は Provisioning Profile が存在しないので、 作るところまでやらせないと次のコマンドが「見つからない」で落ちる - **`create-certificate` を足さない。** 同じ秘密鍵を要求するので鍵が無い状態では同じ所で止まり、 かつ「無ければ作る」の条件が無いので**ビルドのたびに証明書が増えて3回で上限に達する** - **`use-profiles` は必ず走らせる。** Xcode プロジェクトへ署名の3設定 (`CODE_SIGN_STYLE` / `DEVELOPMENT_TEAM` / `PROVISIONING_PROFILE_SPECIFIER`)を書き込む。 **Flutter が作る Runner ターゲットにはこの3つが1行も無く、プロジェクト全体の側に残っていた 古い `iPhone Developer` だけが効いていた。** これが最初の2回の失敗の原因 ### ipa は `flutter build ipa` で作らない ```yaml - name: Build the ipa script: | xcode-project build-ipa \ --project ios/Runner.xcodeproj \ --scheme Runner \ --export-options-plist $CM_BUILD_DIR/export_options.plist ``` - `flutter build ipa` は archive の途中で **Xcode の自動署名に入り、開発用の証明書を探して落ちる**。 `use-profiles` が入れた手動署名の設定がその経路では使われない - **`flutter build ios --no-codesign` を前に走らせない。** `Runner.app` が「署名なし」で出来上がり、続く archive がそれを作り直す。 3回目の失敗がこれで、**前段の77秒がそのまま捨てられていた** - **`--export-options-plist` は `use-profiles` の書き出し先と必ず揃える。** 食い違うと `Path ... does not exist` で落ちる ### 何が書き込まれたかをログに出す ```yaml xcodebuild -project ios/Runner.xcodeproj -target Runner -showBuildSettings 2>/dev/null \ | grep -E "CODE_SIGN_STYLE|DEVELOPMENT_TEAM|PROVISIONING_PROFILE" \ || echo "(署名設定が1件も見つからない)" ``` 次に落ちたとき、**書き込み漏れなのか別の原因なのかをログだけで切り分けられる。** --- ## TestFlight で実機に入れる **内部テストなら審査も入力も要らない。** 外部テストとは別物。 | | 対象 | 審査 | テスト情報の入力 | | --- | --- | --- | --- | | **内部テスト** | 自分のチームのメンバー(100人まで) | **不要** | **不要** | | **外部テスト** | 誰でも(1万人まで) | **要る**(初回のみ) | フィードバック用メール・担当者の氏名・電話番号 | - **実機確認に要るのは内部テストなので `submit_to_testflight: false` にする。** `true` のままだと **ビルドもアップロードも成功したあとの最後の一手で落ちる** (`App is missing required Beta App Review Information`)。 ビルドをやり直す必要はなく、画面から内部テストへ追加すればよい - **内部テスターになれるのは App Store Connect のアカウントを持つ人だけ。** 友人に配るには外部テストが要る - **招待メールのリンクから TestFlight を開く。** 先に TestFlight アプリを入れて開くと、 招待コードの入力画面から進めなくなる(**内部テストにコードは発行されない**) 根拠は Apple の公式ドキュメント(`developer.apple.com/help/app-store-connect/test-a-beta-version/`)。 審査も入力も**「外部テスターに配る場合」が条件**と明記されている。 **ビルド番号は毎回上げる。** 同じ番号は2度受け付けられない (`The bundle version must be higher than the previously uploaded version`)。 Flutter の `pubspec.yaml` は Android と共通なので、**上げると Play の versionCode も動く。** 2つのストアの番号は互いに無関係なので、揃えるための仕組みは要らない。 --- ## 実機で真っ白になったら **`GoogleService-Info.plist` が Xcode プロジェクトに登録されているか確認する。** ``` grep -c "GoogleService-Info" ios/Runner.xcodeproj/project.pbxproj ``` **0 なら原因はこれ。** ビルド時にファイルを置いても、 **Xcode は「プロジェクトに登録されたファイル」しかアプリに入れない。** `Firebase.initializeApp()` がそれを読んで例外を投げ、`runApp()` に届かないまま止まる。 **クラッシュログは出ない。** Dart の例外は iOS のクラッシュとして記録されないので、 「アプリは生きているが何も描かれない」状態になり、切り分ける材料が何も残らない。 対処は `AppFrameworkInfo.plist` と同じ形で4か所に足す(PBXBuildFile / PBXFileReference / Runner グループの children / Resources ビルドフェーズ)。**ビルドのたびに登録するのではなく 1回コミットする** — pbxproj に入るのはファイルへの参照だけで **plist の中身は入らない**ので、 秘密情報は commit されない。 **あわせて、起動処理を try で囲んで失敗の理由を画面に出す。** 真っ白は情報がゼロで次の一手が決まらない。 **エラー画面はアプリのテーマを使わず Flutter 標準の部品だけで作る** — テーマ側が壊れているとエラー画面まで真っ白になる。 --- ## Android から持ち込むときに見落としたもの **iOS は Android と依存の集合が丸ごと違う。** Android で通した確認をそのまま流用できない。 署名だけを見ていて、4件を見落としたまま App Store Connect へ上げ、作り直しになった。 - **広告SDKのアプリ ID と広告ユニット ID。** Flutter の雛形には Google の**公開テスト用 ID** が入っている。**iOS 用は別に発行が要る** - **ライセンス一覧。** `registerAndroidLicenses()` をプラットフォーム判定なしで呼ぶと、 **iOS で存在しない Android の部品を表示し、実在する iOS の SDK を1件も載せない**状態になる - **Mac が無いと `Package.resolved` が手元に無い。** iOS 側が何を取り込むかは **Codemagic のビルドログの `Fetching from ...` を読む** - **`.binaryTarget` に注意。** Google の広告SDKはリポジトリの LICENSE が Apache 2.0 だが、 `Package.swift` が組み立て済みの部品を指しており、**実際に入るものには Google の利用規約が適用される。** **リポジトリの LICENSE だけ読むと Apache と書いて誤りになる** - 実測では**3件が Apache 2.0 ではなかった**(LevelDB=BSD 3-Clause、nanopb=zlib、広告SDK=Google の規約) - **輸出コンプライアンスの申告**(`Info.plist` の `ITSAppUsesNonExemptEncryption`)。 無いと**アップロードのたびに画面で聞かれる**。独自の暗号化が無ければ `false` - **ATT の説明文(`NSUserTrackingUsageDescription`)があるのに要求するコードが無い**状態に注意。 ダイアログは出ない。画面が体験と食い違う形なので、どちらかに揃える --- ## 環境 - Flutter 3.47.1 / Xcode latest / CocoaPods default - Codemagic `mac_mini_m2` - 開発機は Windows(Git Bash)。Mac は1台も使っていない - 2026-08 時点の記録