WhalebirdをReactで書き直した

長らくVue.jsだったWhalebirdをReact.jsで書き直した.というよりNext.jsになった.

github.com

当初,Svelteで書き直してみたんだけど,これがあまり思い通りにはならないことがわかって,Next.jsでの書き直しになった. 6.0.0がNext.jsの初版で,6.1.0まで来たところで,まぁ少しは使えるようになってきたので書き残しておく.

まず,最初に書き直しを考えたのは2023年の4月くらいだ.2023年の改修で,今までNode.js側のストアとしてnedbを使っていたものをbetter-sqlite3に置き換えていた.これは単純にセキュリティ的な理由と,SQLの強力さを求めて載せ替えた.

ただ,この選択はあまり正解だったとは言えずに後悔することになった.better-sqlite3はsqlite3を使うことから考えても,native extentionだった.これにより,クロスビルドをすると必要なsqliteのバイナリがパッケージに含まれないという問題が頻発した.

github.com

github.com

github.com

次に,Vue3だ.2022年にかなりの部分をVue3に書き換えたのだが,これがあまり楽しくなかった.もともとVue2の時代は,確かに型定義は全然なかったしエディタの補完も効かない状態ではあったが,Easyという点において秀でるものがあった.まぁそれが,アプリケーションがでかくなるとともに辛くなる要因でもあったので,Vue3みたいなものが求められたのだろうが.Vue3にした結果,たしかにそれなりに型はつかえるようになったが,それでもまだまだ全然型がつかない場所は多く,結局Vue2のEasyだけが奪われたような感触を感じた.

そのため2023年後半から書き換えを視野にいくつか試してみた.Svelteもその一環ではあったが,結局エディタの補完がよく効く以上の魅力は感じられず,またVueみたいなものを作るのであればReactに移行することにした.

Next.jsを選んだことに大きな理由はないが,Electronに乗せる都合上,枯れているというのは大きい.

github.com

あと,Next.jsでもReact.jsでもRemixでもなんでもいいのだが,昔ながらのSPAで,Client-side Renderingをしたいというのがある.そういう意味でもSvelteは選択肢から外した.

結局ほぼすべてのコードが書き直しとなった.その結果,現状ではかなり軽くなっている. あと,以前と違ってほぼすべてのロジックをフロント側に持ってきた.これによりipcも最小限になっている.まぁ結果としてバックグラウンド動作みたいなことはほぼできなくなっているが……そもそもレンダリングが重いからバックグラウンドにしたいわけであって,軽いのであれば不要なんじゃないかな.

cssはtailwindを使っている.これはこれで非常に良いので今後もぜひ使っていきたい.のだが,components libraryがtailwind対応してないと結構盛大にぶっ壊れることがわかったので,採用できるcomponents libraryの選択肢は狭まった.

で,今後実装しなきゃいけないものたち

  • 検索
  • ユーザ名/ハッシュタグ/絵文字の自動補完
  • プロキシ設定
  • フォローリクエストのハンドリング

GitHub ActionsでTauriアプリのSnapパッケージを自動ビルドする

Tauriでアプリを作っているんだけど,snapパッケージをビルドしてSnap Storeにアップロードしたいと思った.

しかしドキュメントにはsnapに関する記述がない.

tauri.app

また,Tauriは公式でGitHub Actionsを提供していて,こいつを使ってビルドできるのだが,こいつもsnapをサポートしている様子がない.

github.com

tauri-snap-packager

というわけで別のパッケージを使ってsnapをビルドするわけだが,

github.com

こいつを使ってみた.

ただし,これにはいくつか問題点がある.

  1. multipassが必要になる
  2. ビルドするとsnapcraft.ymlが生成されるが,grade: 'devel' 指定のため,このままだと snapcraft upload できない
  3. summarydescription もデフォルトのままなのでカスタマイズできない
  4. core18に依存しており,かなり古い

1の大きな問題点は,GitHub Actionsだ.GitHub Actionsでmultipassを動かすのはかなり難しい.

github.com

このため,使うのであれば,lxdの方を推奨されており,これはActionsが用意されている.

github.com

しかしここまでクリアしても, gradeの問題は残る. ローカルでやるのであれば,一度 tauri-snap-packager でビルドしたあと,snapcraft.ymlを修正し snapcraft コマンドを再度実行することで再ビルドされる.

しかし,GitHub Actionsでビルドされるとなると,これを毎回シェルスクリプトでやるのは虚しい.

というわけで作った

github.com

独自のtauri-snap-packagerを作った.

差分としては,

  1. core20にアップデート
  2. grade: 'stable'
  3. summarydescriptionはtauri.conf.jsonから取ってくる

状態にしたので,ビルドしたらそのままアップロードできる.

全体としてはこんな感じ

jobs:
  snap:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    env:
      SNAPCRAFT_BUILD_ENVIRONMENT: lxd
      SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Node.js setup
        uses: actions/setup-node@v4
        with:
          node-version: 20
      - uses: pnpm/action-setup@v2
        with:
          version: 8
      - name: Rust setup
        uses: actions-rs/toolchain@v1
        with:
          toolchain: stable

      - name: Install dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
      - name: Setup LXD
        uses: canonical/setup-lxd@main
        with:
          channel: latest/stable
      - name: Install snapcraft
        run: |
          sudo snap install snapcraft --classic
      - name: Install app dependencies and build
        run: pnpm install --frozen-lockfile && pnpm tauri build

      - name: Build snap
        run: |
          pnpm run snap
      - name: Publish
        working-directory: src-tauri/target
        run: |
          snapcraft upload ./*.snap --release beta

これでGitHub Actionsから自動でsnapパッケージをビルドして,betaにアップロードするところまでやってくれる.

Native DependencyがあるElectronアプリをビルドする

Electronでsqliteを使いたくなった場合,

github.com

とか

github.com

とかを使うと思う.これを組み込んだ状態でアプリをビルドするときに,かなり詰まったので記録しておく.

成果物と同じプラットフォームでビルドする場合は問題なし

たとえばLinuxLinux用のビルドをするとか,WindowsWindows用のビルドをするような場合,特になんの問題もない.上記のライブラリはどちらもnative dependencyになるのだが,同じプラットフォーム向けなのでビルドされたバイナリもそのまま動く.

問題はそうではない場合が,かなりきついという話.

MacOS x64でarm64向けのビルド

ここがかなり詰まった部分になる.今まで,

  • DMG: electron-builder + electron-notarize
  • AppStore: electron-packager + electron-universal + codesignコマンド

でビルドしていた.electron-builderは,archを指定した場合にはarchごとにrebuildをしてくれている.しかしelectron-packagerは本当にパッケージングするだけなので,x64マシンでビルドした成果物はarm64では動かない.arm64向けにnative dependencyをrebuildしなければならず,electron-rebuildを使って

github.com

こいういうことをする必要がある.そのため,最初に失敗したのはAppStore版であった(後にDMGも結局動かないことが発覚したので,いずれにしろ今までの設定ではダメなことに変わりはなかった).

同時に複数の問題が発生しており,時系列順に書くとかなり複雑なため,時系列をすっ飛ばして発生した問題を整理する.問題点が4つある.

問題1: electron-osx-signでAppStore版をcode signingするとローカルで起動できない

electron-builderでもelectron-packagerでも良いのだが,ビルドしたあとにcode signingする必要がある.DMGとして配布するのであれば Developer ID Application: YOUR_NAME(TEAM_ID) という証明書でcode signingできれば良い. AppStore版の場合は 3rd Party Mac Developer Application: YOUR_NAME(TEAM_ID) という証明書を使う.

DMG版はelectron-osx-signでcode signingしたあと,普通に起動できるのだが(当然だ),AppStore版については EXC_CRASH (SIGKILL (Code Signature Invalid)) というエラーによりエラーがでて起動できない.これを避けるためにもともと electron-packager + codesignコマンドを使っていた.

参考: https://github.com/h3poteto/whalebird-desktop/blob/4.7.4/appStore.sh

が,どうやら,electron-osx-signにおいてこの挙動は想定内であり,通常の挙動らしい.つまりAppStore用のビルドはAppStore経由出ない場合には起動できない.ではどうするかというと,ProvisioningProfileとともにcode signingした上で,AppStoreConnectにアップロードし,TestFlight経由でインストールすると起動する(その他の設定が正しければ).

というわけで,この時点でelectron-packager + electron-universal + codesignコマンドを捨てて,electron-builderでAppStore版もビルドすることにした.

問題2: native depencencyの成果物もcode signingしないとエラーになる

どういうことかというと,node-sqlite3もbetter-sqlite3もrebuild時に .node ファイルを生成し,electronはこいつを呼び出してsqliteを実行する.このときに呼び出す .node は外部ライブラリ扱いであり,最近のMacOSでは依存している外部ライブラリもcode signingされている必要がある.

これをやらないと,

Uncaught Exception:
Error: dlopen(/var/folders/w9/_481zfb94wx2yq562f5h68vw0000gn/T/.social.whalebird.app.mH2BlU, 0x0001): tried: '/var/folders/w9/_481zfb94wx2yq562f5h68vw0000gn/T/.social.whalebird.app.mH2BlU' (code signature in <5439AE43-90A3-3A47-A95F-32AEC2235239> '/private/var/folders/w9/_481zfb94wx2yq562f5h68vw0000gn/T/.social.whalebird.app.mH2BlU' not valid for use in process: mapped file has no Team ID and is not a platform binary (signed with custom identity or adhoc?)), '/System/Volumes/Preboot/Cryptexes/OS/var/folders/w9/_481zfb94wx2yq562f5h68vw0000gn/T/.social.whalebird.app.mH2BlU' (no such file), '/var/folders/w9/_481zfb94wx2yq562f5h68vw0000gn/T/.social.whalebird.app.mH2BlU' (code signature in <5439AE43-90A3-3A47-A95F-32AEC2235239> '/private/var/folders/w9/_481zfb94wx2yq562f5h68vw0000gn/T/.social.whalebird.app.mH2BlU' not valid for use in process: mapped file has no Team ID and is not a platform binary (signed with custom identity or adhoc?)), '/private/var/folders/w9/_481zfb94wx2yq562f5h68vw0000gn/T/.social.whalebird.app.mH2BlU' (code signature in <5439AE43-90A3-3A47-A95F-32AEC2235239> '/private/var/folders/w9/_481zfb94wx2yq562f5h68vw0000gn/T/.social.whalebird.app.mH2BlU' not valid for use in process: mapped file has no Team ID and is not a platform binary (signed with custom identity or adhoc?)), '/System/Volumes/Preboot/Cryptexes/OS/private/var/folders/w9/_481zfb94wx2yq562f5h68vw0000gn/T/.social.whalebird.app.mH2BlU' (no such file), '/private/var/folders/w9/_481zfb94wx2yq562f5h68vw0000gn/T/.social.whalebird.app.mH2BlU' (code signature in <5439AE43-90A3-3A47-A95F-32AEC2235239> '/private/var/folders/w9/_481zfb94wx2yq562f5h68vw0000gn/T/.social.whalebird.app.mH2BlU' not valid for use in process: mapped file has no Team ID and is not a platform binary (signed with custom identity or adhoc?))
at process.func [as dlopen] (node:electron/js2c/asar_bundle:5:1812)
at Module._extensions..node (node:internal/modules/cjs/loader:1205:18)
at Object.func [as .node] (node:electron/js2c/asar_bundle:5:2039)
at Module.load (node:internal/modules/cjs/loader:988:32)
at Module._load (node:internal/modules/cjs/loader:829:12)
at c._load (node:electron/js2c/asar_bundle:5:13343)
at Module.require (node:internal/modules/cjs/loader:1012:19)
at require (node:internal/modules/cjs/helpers:102:18)
at Object.<anonymous> (/Applications/Whalebird.app/Contents/Resources/app.asar/node_modules/sqlite3/lib/sqlite3-binding.js:4:17)
at Module._compile (node:internal/modules/cjs/loader:1120:14)

というようなエラーになる.

github.com

ここでは com.apple.security.cs.disable-library-validation が紹介されているが,BigSur以上のMacOSの場合は,この設定に問題がある件が指摘されている.

https://github.com/electron-userland/electron-builder/issues/3940#issuecomment-900527250

で,そもそもなぜアプリに含まれているはずの .node ファイルがelectron-osx-signでcode signingされないのかというと,これらがすべて asar に圧縮されてしまっているためだ.electronはプログラムを asar に圧縮して配布している.割と簡単に解凍できるのだが,code signするときにいちいち解凍はしてくれない. というわけで,

"build": {
  "mac": {
    "asarUnpack": "node_modules/**/*.node"
  }
}

という設定を入れて,.node ファイルを asar 圧縮から除外してやる必要がある.これをやると asar.unpacked というディレクトリ内に生ファイルのまま格納されるので, electron-osx-sign--deep オプション付きでcodesignするときにすべてcode signingされる.

ちなみに元のcodesignコマンドを使ったスクリプト--deep オプションを使っていなかったので,unpackedにしたところで結局同じエラーに陥った.

問題3: native dependencyの成果物をuniversalでマージできない

electron-universalは,一度x64,arm64両方のビルドを作り,それらをマージすることでuniversalバイナリを作っている.このときに,問題2でasarから除外したunpackedなnative dependencyが問題になる.

Detected unique file "node_modules/sqlite3/lib/binding/napi-v6-darwin-unknown-arm64" in "***/whalebird-desktop/build/mac-universal--arm64/Whalebird.app/Contents/Resources/app.asar.unpacked" not covered by allowList rule: "undefined"

github.com

これと同じことが発生し,同じ名前の別archファイルをマージできなくなる.これについては,singleArchFiles オプションを使えという話ではあるが,最終的に俺は mergeASARs: false にして,universalのときにasarのマージをやめた.これは次の問題4に大きく関係する. が,いずれにしろ,どちらかのオプションを使い,両方のアーキテクチャ用のバイナリを残す必要がある.

問題4: node-sqlite3はelectron-rebuildした成果物が正しくない

ここまでで,だいたい動くようにはなったのだが,一つ大きな問題が残った.どうもx64では問題なくarm64でのみ問題が発生することがわかった.

エラーがこれだ.

Uncaught Exception:
Error: Cannot find module '/Applications/Whalebird.app/Contents/Resources/app.asar/node_modules/sqlite3/lib/binding/napi-v6-darwin-unknown-arm64/node_sqlite3.node'

おかしい.そもそも問題3の解決のために node_sqlite3.node は asarから除外してunpackedに入っているはずだ.しかし,ここではasarの中を探している.

さらにもう一点おかしいことがある. unpackedディレクトリの中身はx64もarm64も同じで,bindingの下には

  • napi-v{napi_build_version}-darwin-unknown-arm64
  • napi-v{napi_build_version}-darwin-unknown-x64
  • napi-v6-darwin-unknown-x64

の3つしかディレクトリがない.確かにこれなら,x64は napi-v6-darwin-unknown-x64 を使って起動できるが,arm64になると napi-v6-darwin-unknown-arm64が存在せずにエラーになることは理解できる.asarを探しているのは,おそらく探索順序が unpacked -> asar の順で探索されているだけだろう(これは勘).

というわけで

github.com

これだ.これはelectron-rebuildとnode-sqlite3の掛け合わせで発生する問題のようだ.

これ自体が解決していないので,そもそもnode-sqlite3を使うのをここでやめて,better-sqlite3を使うことにした.

better-sqlite3は開発時もrebuildしないと起動しない

github.com

これはproduction buildの話ではないのだが,そもそも開発環境でも

NODE_MODULE_VERSION 108. This version of Node.js requires
NODE_MODULE_VERSION 107. Please try re-compiling or re-installing

というエラーがでて起動できなかった.これはelectronのバージョン(の中で決まっているNODE_MODULE_VERSION)が合致したものを使えばいいとは思うのだが,毎回それを気にしてライブラリの更新をするのも手間なので,install直後にrebuildすることにした.

package.json

  "scripts": {
    "postinstall": "electron-builder install-app-deps"
  }

とかを追記しておけば,起動するようになる.

解決

そして解決し,無事にAppStoreのレビューも通った.

今回の問題のうち

  • 問題1

はAppStoreのみに関係する問題だったが,

  • 問題2
  • 問題3
  • 問題4

に関しては,DMGでも同様のエラーがでて使えないという報告が上がっており,

Whalebird 5.0.0 fails to start · Issue #4195 · h3poteto/whalebird-desktop · GitHub

Launching Whalebird 5.0.0 universal on macos 13.2.1 arm64 m2 fires uncaught exception · Issue #4183 · h3poteto/whalebird-desktop · GitHub

v5.0.0 macOS x64 JavaScript error on launch · Issue #4173 · h3poteto/whalebird-desktop · GitHub

同時にすべて解決することができた.

まとめ:結局どうするのが良いのか

node-sqlite3ではなくbetter-sqlite3を使う

Windows, Linux向け

electron-builderでもelectron-packagerでも問題ない.そもそもビルド環境と実行環境が同じプラットフォームであれば,気にすることはない.

MacOS向け

AppStore

electron-builderを使う.

こういう設定でビルドする.

{
  "productName": "Whalebird",
  "appId": "social.whalebird.app",
  "artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
  "directories": {
    "output": "build"
  },
  "extraResources": [
    "build/icons/*"
  ],
  "files": [
    "dist/electron/**/*",
    "build/icons/*"
  ],
  "mas": {
    "type": "distribution",
    "entitlements": "plist/parent.plist",
    "entitlementsInherit": "plist/child.plist",
    "entitlementsLoginHelper": "plist/loginhelper.plist",
    "hardenedRuntime": false,
    "gatekeeperAssess": false,
    "extendInfo": {
      "ITSAppUsesNonExemptEncryption": "false"
    },
    "provisioningProfile": "./packages/socialwhalebirdapp_MAS.provisionprofile"
  },
  "mac": {
    "icon": "build/icons/icon.icns",
    "target": [
      {
        "target": "mas",
        "arch": [
          "universal"
        ]
      }
    ],
    "category": "public.app-category.social-networking",
    "hardenedRuntime": true,
    "gatekeeperAssess": false,
    "darkModeSupport": true,
    "extendInfo": {
      "ITSAppUsesNonExemptEncryption": "false"
    },
    "mergeASARs": false,
    "asarUnpack": "node_modules/**/*.node"
  }
}

なお,electron-builderは内部でelectron-osx-signやelectron-rebuild, electron-universalを使っている.そのため,

com.apple.security.application-groupsElectronTeamID は自動挿入されるので,entitlementsに追加する必要はない.

whalebird-desktop/plist at master · h3poteto/whalebird-desktop · GitHub

この辺を参考にしてもらうと良い.

DMG

electron-builderを使う.

こちらもあまり変わらないが

{
  "productName": "Whalebird",
  "appId": "social.whalebird.app",
  "artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
  "directories": {
    "output": "build"
  },
  "extraResources": [
    "build/icons/*"
  ],
  "files": [
    "dist/electron/**/*",
    "build/icons/*"
  ],
  "afterSign": "build/notarize.js",
  "dmg": {
    "sign": false,
    "contents": [
      {
        "x": 410,
        "y": 150,
        "type": "link",
        "path": "/Applications"
      },
      {
        "x": 130,
        "y": 150,
        "type": "file"
      }
    ]
  },
  "mac": {
    "icon": "build/icons/icon.icns",
    "target": [
      {
        "target": "dmg",
        "arch": [
          "x64",
          "arm64",
          "universal"
        ]
      }
    ],
    "category": "public.app-category.social-networking",
    "entitlements": "plist/entitlements.mac.plist",
    "entitlementsInherit": "plist/entitlements.mac.plist",
    "entitlementsLoginHelper": "plist/loginhelper.plist",
    "hardenedRuntime": true,
    "gatekeeperAssess": false,
    "darkModeSupport": true,
    "extendInfo": {
      "ITSAppUsesNonExemptEncryption": "false"
    },
    "mergeASARs": false,
    "asarUnpack": "node_modules/**/*.node"
  }
}

こんな感じ.

Siderが終わるのでlintをreviewdogに移した

このとおりですが,

siderlabs.com

siderがサービス終了するらしいです.

今まで個人プロジェクトでも,会社のプロジェクトでも結構お世話になってきました.使い始めた頃は,まだSideCIという名前で,会社名もアクトキャットという名前だった気がしますが,感慨深い.

とりあえず使えなくなるので,移行します.

続きを読む

AWS Global AcceleratorをKubernetesのリソースから管理する

AWS上に作ったKubernetesでサービスを外部に公開する方法はいくつか存在する.簡単にやるならServiceをtype: LoadBalancerで定義すればNetworkLoadBalancerが作れるし,aws-load-balancer-controllerを使えばApplicationLoadBalancerも作れる.ただ,このLBの前段にGlobalAcceleratorを作りたくなった場合はどうしたらいいだろうか. NLBにしろALBにしろ,Kubernetes内のServiceやIngressの定義に応じて動的に作成された場合,作成された後にGlobalAcceleratorのEndpointGroupに登録する必要があるので,毎回手動作業が発生してしまう. というわけで,これを解決するOSSを作った.

github.com

続きを読む