eaglesakuraの技術ブログ

技術的な話題とか、メモとか。

Flutter 3.41.x -> 3.44.x にマイグレーションしたときのハマりどころと解決方法

3.44系の大きな変更

  • Android Gradle Plugin(AGP) 9.x系がサポートされた
  • Swift Package Managerが有効化された
  • Dartバージョンが更新された
    • AnalyzerやFormatterも更新された

Androidビルド / AGP移行のハマりどころ

  • Android + Flutterのビルドシステムの都合上、PluginすべてがAGPをサポートしないと完全移行はできない
  • 案の定、サポートしていないPluginがあり、完全移行はできなかった
  • しばらくAGP8.x系で互換モードを頼りにしておく
  • AGP 10.x系では互換動作がなくなるため、そのときは覚悟を決めよう

iOSビルド / Swift Package Managerの移行のハマりどころ

  • 今回一番のハマりどころ
  • もともとXcodegenを使用して、yamlでプロジェクト構成を管理していた
    • FlutterのFlavor制御 (Configuration, Schema)コントロールはこっちのほうが便利だった
  • 依存しているPluginは、最新バージョンでは全てSPM対応済みだった
    • これがハマりどころだった
  • Podsが不要になったことにより、 Runner.xcworkspace が自動生成されなくなってしまった
  • Flutterのビルドシステムは Runner.xcworkspace が必須だった
  • 更に、Firebase Pluginの不具合 に引っかかり、正常ビルドが行えないことが重なって、問題の切り分けに時間がかかった
  • SPM自体を使うのが初めてだったので、yaml構成が手探りだった

具体的な対応事項

Flutter / Dart SDK の更新

  • ルートおよび全パッケージの pubspec.yaml で Dart >=3.12.0 / Flutter >=3.44.0 を要求するよう更新した
  • pubspec.lockdependency_overrides の Firebase 系パッケージを追随させた(例: firebase_core 4.7.0 → 4.9.0firebase_auth 6.4.0 → 6.5.1
# .mise.toml
flutter = "3.44.0"
# pubspec.yaml (各パッケージ共通)
environment:
  sdk: ">=3.12.0 <4.0.0"
  flutter: ">=3.44.0 <4.0.0"

Android: AGP 9 完全移行は見送り、built-in Kotlin への準備のみ

  • Flutter migrator が追加した AGP 8.x 互換フラグ を明示的に残した(完全移行は Plugin 側の追随待ち)
  • build.gradle.ktskotlinOptionskotlin { compilerOptions { ... } } ブロックへ移行した(built-in Kotlin 移行の下準備)
  • AGP 9 昇格時に削除すべき箇所へ NOTE コメントを追記した
# app/android/gradle.properties
# AGP 9 昇格時: 以下2行を削除し built-in Kotlin / new DSL を有効化する。
android.builtInKotlin=false
android.newDsl=false
// app/android/app/build.gradle.kts
// AGP 9 昇格時: kotlin-android を削除し built-in Kotlin へ移行する。
id("kotlin-android")

kotlin {
    compilerOptions {
        jvmTarget.set(JvmTarget.JVM_17)
    }
}

iOS: Swift Package Manager への暫定移行

  • PodfilePodfile.bak に退避し、Podfile.lock を削除した(CocoaPods 依存の整理)
    • これはSPM非対応のPlugin追加時に、すぐに原状回復できるようにするため
  • project.yml に SPM パッケージ定義を追加し、Pods_Runner.framework 依存を FlutterGeneratedPluginSwiftPackage へ差し替えた
  • Firebase iOS SDK の推移的依存で 12.14.0 に上がるとビルド失敗するため、firebase-ios-sdk / googleappmeasurement12.13.0 に仮固定した
  • Runner.xcworkspace が自動生成されなくなった問題に対し、project.ymlpostGenCommand から postGenCommand.sh を呼び出し、テンプレートから workspace を手動生成するよう変更した

    # app/ios/project.yml
    options:
      postGenCommand: xcodegen/scripts/postGenCommand.sh
    
  • Flavor ごとの Scheme preAction を schemeTemplates で共通化し、各 Flavor の Scheme から xcode_backend.sh prepare を実行するよう変更した

    • project.ymlincludebase.yml と Flavor 別 yaml を合成する構成はそのまま維持した
    • base.ymlFlutterFlavorSchema テンプレートを定義し、CocoaPods 向け preAction はコメントアウトして SPM 向け prepare のみ有効化した
    • development.yml など各 Flavor yaml の schemes からテンプレートを参照する
    # app/ios/project.yml
    include:
      - xcodegen/base.yml
      - xcodegen/local.yml
      - xcodegen/development.yml
      - xcodegen/production.yml
    
    # app/ios/xcodegen/base.yml(project.yml 経由で include)
    schemeTemplates:
      FlutterFlavorSchema:
        build:
          preActions:
            - name: "[Flutter] Run Prepare Flutter Framework Script"
              settingsTarget: Runner
              script: '"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare'
    
    # app/ios/xcodegen/development.yml
    schemes:
      development:
        templates:
          - FlutterFlavorSchema
        build:
          targets:
            Runner: all
        run:
          config: Debug-development
        archive:
          config: Release-development
    
  • check_pods_manifest_lock.sh は Podfile 不在時にスキップするよう修正した(SPM 移行後も Build Phase を残すため)

# app/ios/project.yml
packages:
  FlutterGeneratedPluginSwiftPackage:
    path: Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage
  # NOTE. 推移的依存解決により、12.14.0にされると、ビルドが失敗するため、バージョンを仮固定している
  firebase-ios-sdk:
    url: https://github.com/firebase/firebase-ios-sdk
    version: 12.13.0
targets:
  Runner:
    dependencies:
      - package: FlutterGeneratedPluginSwiftPackage
# app/ios/xcodegen/scripts/postGenCommand.sh
# Flutter SPM: ephemeral が無いと Xcode が SPM 解決に失敗する。
SPM_PACKAGE="${IOS_DIR}/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage/Package.swift"
# ...
# Pods なし workspace(Runner.xcodeproj のみ)
cp "${TEMPLATE}" "${WORKSPACE_DATA}"

Dart Analyzer変更への追随

  • analysis_options.yamlbuild/** を exclude 追加した
    • これは原因がよくわからないが、アップデート後に build/** が反応するようになったため

この記事の試み

  • 具体的な対応事項 のブロックは、AI AgentにPull Requestから生成させることを試した
  • 技術ブログを書く人間としての誠実さと、記載したい内容の密度のバランス的にはこんなもんだろう

自分用のAI-Agent SKILLの公開と、Prompt Requestへの期待

github.com

Skillを公開する意味

  • AI Agentによって、誰でもコードを書けるようになった
  • 「コードを書ける」のと、「適切なコードを書ける」のは全く違う
  • 「適切なコードを書くために、自分はこうしている」というポートフォリオの意味で公開している

AI Agentによる「専門領域を安易に壊す」ことへの懸念

  • AI Agentは非常に便利で、自分の実力を何倍にも見せてくれる
  • 自分の専門外の分野(別なレイヤーを担当しているチーム)に手を出せる希望を、AI Agentは持っている
  • 受け取った側はどうだろうか?門外漢がAI Agentで一発出ししたコードを受け取っても、正直扱いに困るのではなかろうか
    • 開発チームや専門分野にはそれぞれベストプラクティスがあり、開発ルールがあるのだ。
    • 誰でも読み書きできる Cloud Storage設定を作ったり、Injectionの危険があったり、はたまた「自動生成されるコードを、AI Agentが根性で書いてしまった」ようなことも起こり得る
    • ルールを無視したPRをマージするわけにも行かず、かといってそのまま却下するには心理的障壁が高い
    • 子供のお手伝いと同じで、「頑張ったね」とは言えるけど、それをプロダクションコードとするにはリスクが大きすぎるのだ
    • 自分の実力を勘違いしてはならず、各チームは、各分野のエキスパートであるというリスペクトを持たなければならない
    • ランバ・ラルも言ってたでしょ、 見事だな!しかし小僧、自分の力で勝ったのではないぞ!そのモビルスーツの性能のおかげだという事を忘れるな!! って。
  • それでも、AI Agentを利用することで専門外の人間が語りやすくなるメリットもある
  • AI Agentが適切に問題を切り分けたり、なるべくマトモな提案をするためには、知識を持った専門家(エンジニア)が、AI AgentにSKILLとしてナレッジを与えるのが重要であるという考え

Prompt Request による、領域を超えたコミットメントへの期待

  • 直接的なコードを与えるのではなく、 Prompt Request でプロンプトとして食わせられるコンテキストをお互いにやり取りするという概念に期待を持っている
  • 問題点の抽出や、ある程度の提案はAI Agentの一発出しや、門外漢の提案(ベストプラクティスを無視した提案だとしても)でも「ありがとう」と受け取れる
    • 問題点だけ確認して、提案は捨てても良いし、専門家が自身(自チーム)の開発フローに基づいて対処すれば良い
    • AI Agentはいい感じに提案ドキュメントを書いてくれるし、提案ドキュメントそれ自体は本番環境を壊さないし、セキュリティリスクもない(APIキーを書くなよ、とかそういうレベルを除く)
  • Markdown等の共通フォーマットでやり取りすることで、受け取った側は問題解決へのヒントを得つつ、ベストプラクティスに基づいた実装を行える
    • 不具合対応で最も大変なのは、問題点を抽出するところだ
    • AI Agentは、この部分を(たとえ門外漢であったとしても)かなりサポートしてくれる
  • コード変更のリスクは、専門家が適切に対処するのが最善であるのだ。彼ら(そして我々)は専門家なのだ。

自分の実力を過信しすぎず、専門家へのリスペクトを持って、AI Agentの利用をしていきたい。 そしてCursor、君はちょっと課金額が高くてつらいのだ。安くしてくれ頼む。

2026-01のCursorの使い方

Cursorで使っているModel

会社

  • 予算
    • いっぱいだけど無限ではない
  • Sonnet 4.5 / Opus 4.5 / Gemini3Pro / Composer-1を時と事情で使い分ける
  • 基本Sonnet 4.5でオーケストレーション
    • Sonnetが提案し、手製MAGIシステムが複数並列でレビュー&改善して、その結果をSonnetとOpusが反映する、みたいなイテレーションを構築した
      • Sub AgentがBackground対応なので、3つのSub Agentを起動し、別々のContext/プロンプトからレビュー結果を得て、結果を統合している
      • 同一問題に対して、n票以上の同一内容レビューがあがってきたら自動適用、みたいなことをうまいことチューニングしてる
    • 結果、めちゃくちゃ品質と課金額が上がったので、MAGIシステムの出力を弱くして調整してる
    • Opusのほうが一発出しの頭はいいが、「俺が歯車の一部になる」という気持ちで課金額を抑えるチューニングをしてる
  • 以前の3倍くらいのタスクを、10倍くらいの課金で実行してる感じ
  • Sub AgentとSKILLはかなりありがたい機能だが、予算に応じて出力を調整しないとしんどい

個人で作ってるアプリ

  • 予算
    • Pro($20) + 従量課金 $30前後
  • Gemini 3 Flash / Composer-1
  • 安い。ひたすら安い
  • しかし確実性は圧倒的にSonnetやOpusよりも低い
  • 稀によく大暴走して、無限ループに陥る
  • 稀によく大暴走して、「調査しろ」って言ったのに勝手にコードを書き換える
  • 稀によく大暴走して、「計画しろ」って言ったのに勝手にコードを書き換える
  • それでも背に腹は代えられないほどに安い
  • ひたすらMAGIシステムのサブセット、かつ勝手なことをしないように教育しつつ、予算額を抑えるチューニングをして使ってる
  • お小遣いがしんどいでほんま

Cursorの課金体系

  • $20がProプランで、 $60がPro+プラン
  • 3倍の課金額だけど、差分を見ると 3倍の使用量 って書いてて、それは当たり前体操過ぎないだろうか

久々にアプリをリリースした話

リリースしたアプリ「やさしい漢字」

漢字の書き取りのために拡大表示する機能と、その漢字が何年生で習ったのかを表示するだけのシンプルなアプリ

なんでアプリを作ったのか

  • 自分自身、漢字の書き取りが苦手である
    • 子供の学習の手伝いすらできないレベルで苦手
  • 辞書で調べても字が小さい。もう少し大きく表示させたい
  • いつもスマホで調べてるが、拡大表示できたほうが便利
  • じゃあ作ろうか
  • という流れ

直接配布はできないのか?

  • 子供のスマホが、子供向けアカウントとしてセットアップされている
  • 色々面倒がありそうだし、子供の端末に裏口を作りたくない
  • デバッグモードにするより、正規ルートで配信することを目指してみた

仕様

  • もともとは結構いろんな機能があったが、「漢字」に絞ることで仕様を単純化してリリースすることを目指した
  • 目指していた機能については、アップデートで対応していく予定

技術

  • せっかくなので、Flutter + マルチプラットフォームで開発している
  • Webアプリは、子供にブラウザを使わせたくないので対応していない
  • iOS対応しているのは自分自身の学習のためでもある

Cursorの活用

  • 開発初期〜設計が確定する中期まではCursorのProプラン = $20 / 月を活用していた
  • ある程度アプリとしての設計が固まったここ1ヶ月くらいはComposer-1 + Slash-Commandの組み合わせで実装の7割くらいをAI Agentに任せる運用にしている
    • 開発速度は上がったが、財布が干上がる速度も上がった
    • 今では$20 / 月 + オンデマンド $10〜$20程度を課金している

AI Agent活用の所管

  • 設計を任せるのはまだ難しい。あっという間にContext Windowが枯渇して、全体を俯瞰するのがつらくなる。
  • 実装を任せることはできる
  • AI Agentが「設計」をしないようにタスクを切るのが良い
    • 最初に計画ファイルを作らせる -> 計画を監査(手動・自動両面で)する -> サマライズする -> 実装する
    • というステップを踏むと、Context Windowが足りないなりに実装だけを行わせられる
    • アーキテクチャレイヤーを明確に区切っているので、AI Agentには「データ層の作業を切り出す」「ユースケース層と、それ以外でタスクを分割」のように指示することでスコープを小さく指示できる
  • マルチモジュールを活用 + DIを活用することで、AI Agentが食べるContextのスコープを区切ることで対処しやすくなった
  • 初期に可能な限り(自分にとって)キレイな状態のプロジェクトとリファレンスとなる実装を作っておくことが重要で、ここで妥協しなかったのが大きいと思われる
  • 「完璧ではないが、自分にとって70点くらいの出力をコンスタントに出せる」くらいまでにはなってきてる
    • ただし金も出ていく

その他のAI活用

  • アイコン作成はGeminiにやらせた
  • アイコンの背景とかの調整はPhotoshopのAIにやらせた
  • アプリをリリースするだけで何枚画像を作らせるねん

今後の展望

  • 生成AI(Firebase AI Logic)を使った機能を予定しているが、悪意ある第三者の課金攻撃に耐えられる設計にしないといけないので仕様を調整しなければならない
  • 一番下の子供が小学校を卒業するまで、多分コツコツとメンテしていきそう
  • 楽しんでいろんな機能を作れるので、ヨシ

複数のgit秘密鍵を1passwordを経由して切り替える

なぜ必要なのか

  • Githubアカウントを複数所有している
    • 個人で使用しているアカウント
    • 本業用アカウント
    • 副業用アカウント×複数
  • 案件ごとに適切なアカウントを使用して作業を行わなければならない

1passwordでの設定

  • 公式の案内に従って1passwordをセットアップ
  • 次のコマンドを使用して1passwordの認証画面が起動すればOK
ssh -T git@github.com

開発者向けSSH キー | 1Password

アカウントごとにssh-keyを生成/登録する

  • 1passwordで 新規アイテム > sshキー で生成できる
  • Githubのアカウントごとに生成して登録する
    • Auth/Sign両方に登録が必要

異なるVault(保管庫)に切り分けている場合

  • agent.tomlを開く(SSHキーの設定から開ける)
  • Vaultの名称を登録する
[[ssh-keys]]
vault = "プライベート"

[[ssh-keys]]
vault = "Hoge企業用アカウント"

cloneしたディレクトリごとにconfigを切り替える

  • 任意のディレクトリ配下に .gitconfig を配置し、上書きするconfigを設定する
[user]
    name = eaglesakura-override
    email = example@example.com
   signingkey = 署名用のキーの公開鍵
  • ~/.gitconfig に追記
[includeIf "gitdir:path/to/directory/"]
    path = path/to/directory/.gitconfig

[includeIf "hasconfig:remote.*.url:git@example.github.com:example/**"]
    path = path/to/directory/.gitconfig

github.com内で複数アカウントを切り替える

  • enterpriseではなく、普通にgithub.comアカウント複数を適宜切り替える場合
  • これをアカウント数だけ追加する
Host example-account.github.com
  HostName github.com
  IdentityFile ~/.ssh/example-account.pub
  • IdentityFileは公開鍵だけexportして書き込めば良い
  • permission 600にする
  • cloneするときにホスト名を example-account.github.com に書き換えればOK

完了

パスワード管理をGoogleから1Passwordに移行した

パスワードマネージャにGoogleを使っていた理由

  • 無料で使える
    • 一番大きい
  • Androidがメイン端末なので、Chrome(ブラウザ)とアプリが同一のパスワードマネージャを利用できる
  • デフォルトで用意されていて特別な準備をする必要がない

Googleパスワードマネージャから離れたかった理由

  • 前提として、Googleのセキュリティ自体は信用している
  • ただし、Googleの運営は信用していない
    • (課金有無にかかわらず)Google個人アカウントはGoogleの独断によって削除される恐れがあり、なおかつ復旧が難しい
  • 金融機関のID/PASS等、Googleに預けている資産の中でパスワードマネージャのデータが最も価値が大きくなってしまった
  • この状態でGoogleに預け続けることは、人生のリスクが大きすぎる

1Passwordを選定した理由

  • 転職した先で利用していた
    • 利用に(完ぺきではないが、概ね)問題ないことが確認できた
  • Amazonのセールで家族アカウントが半額セールしていた
    • 買ったアクティベーションコードが無効判定されて、キレながらサポートに連絡したら直してくれた
    • 家族でアカウントが共有できるので、電力とか生協のアカウントを共有しやすくなった
    • 子どものアカウント情報をそれぞれ整理できるのは便利
  • 1Passwordの復元コードは印刷して封をして金庫に入れた

悩みどころ

  • 2FAがgmailだと、アカウントが停止すると詰む
  • Google Account認証を使っていると、アカウントが停止すると詰む

まとめ

  • Google BANリスクに備えよう
  • BANされても死なないようにしておこう

ssh接続もシリアル接続もできなくなったGoogle Compute Engineで動くサーバーを復旧させる

何が起きたか

  • 社内システムの動いているサーバーをメンテしようとした
  • 突然GCEのいつものssh接続ができなくなった
  • 冷や汗をかきながらシリアル接続を有効化するが、ログインできず
  • 自分だけが締め出されたかと思って別ユーザーでも試すがログインできず

状況確認

  • 基本的なステータスチェック(権限、ネットワークとか)はOK
  • ssh接続しようとするとPermissin Deniedする
  • 起動スクリプトを介して公開鍵を登録したりユーザーパスワードをリセットしてもログインできない
  • 基本的なLinux力が足りていない

原因

  • slackのWebhookが使えなくなった(Let’s Encryptの証明書が更新された)関係でいろいろいじったので、心当たりがありすぎる
  • 証明書を再インストールしたあたり、バックアップとらずにそぉい!とやったのがまずかった気がする
  • サーバーのdaemon自体は普通に動き続けていたため、普通に使われている(今日まで気づかなかったし)
  • ここ最近のデータを失うのはツライ
  • データはどうにか救わねばならない

対応

  • このサーバーで動いていたのは社内用のRedmineで、なおかつbitnamiパッケージで導入したので詳しい構成はわからない
  • /opt 配下にいろいろインストールされており、なおかつ正常に動いていた2週間前のバックアップは存在する
  • なので、「正常に動いていた時代のバックアップ」と「最新のデータ」を合体させて起動させれば何とかなるんじゃないか?という対処を考える

やった手順

  1. 壊れたサーバーのSnapshotをとってディスクAを作成する
  2. 2週間前のバックアップからディスクBを作成する
  3. Compute Engineのインスタンス(復旧くん)を新規に作成して、ディスクA/ディスクBをマウント
  4. 復旧くんの中でディスクA/optをディスクB/optにrsync
  5. 復旧君からディスクBを外す
  6. 壊れたサーバーの起動ディスクをディスクBに切り替える
  7. 起動

反省

  • sshから締め出されてもシリアルがあるから大丈夫だべ、と思って油断すると両方から締め出される
  • sudoするまえにバックアップを取ろう
  • メンテしたらちゃんと再起動とsshのログインは確かめよう
  • 2023年最大の冷や汗を体験した