ElixirDesktopのiOSアプリ実装に苦労した話

## はじめに

Elixir Desktop は Phoenix LiveView を使って、 macOS でも Windows でも iOS でも Android でもネイティブアプリっぽいものが実装できるパッケージです

https://github.com/elixir-desktop/desktop

今回は iOS 版のサンプル実装を私のローカルマシン（macOS）上でビルドしようとして、
色々なエラーに引っ掛かった話を書いていきます

https://github.com/elixir-desktop/ios-example-app

## ビルド環境

- macOS Monterey 12.4
- XCode 13.4.1
- asdf 0.10.0

私は色々な案件で色々な言語、ツールをワサワサ使う関係上、
開発言語やツールの実行バージョン管理に asdf を使っています

https://github.com/asdf-vm/asdf

例えば Elixir なら `asdf install elixir 1.13.4-otp-24` というようにするだけで
Elixir の指定バージョンがインストールでき、
`asdf local elixir 1.13.4-otp-24` とするだけで、実行ディレクトリ配下では
Elixir の指定バージョンが使われるようになります
同じことが凡そ全ての開発言語でできるので非常に便利です

## custom OTP のインストール

https://github.com/elixir-desktop/ios-example-app#how-to-build--run

実装例のビルド手順は以下のようになっています

> 1. Install the custom OTP version on your machine (*see known todos)
>
> 2. Install xcode from the app store.
>
> 3. Install brew, elixir, git, carthage, npm
>     ```bash
>     brew install elixir carthage git npm
>     ```
>
> 4. fork / git clone this project to your local disk:
>     ```bash
>     git clone https://github.com/elixir-desktop/ios-example-app.git
>     ```
>
> 5. Build the dependencies:
>     ```bash
>     cd ios-example-app && carthage update --use-xcframeworks
>     ```
>
> 6. Open the ios-example-app project with xcode
>
> 7. Start the App

問題になるのは 1. で指定する custom OTP です

known todos には以下の記載があります

> To have the embedded Erlang match the one you use for compilation you can install the same > version as the embedded:
>
> ```
> mkdir -p ~/projects/
> kerl build git https://github.com/diodechain/otp.git diode/beta 24.beta
> kerl install 24.beta ~/projects/24.beta
> ```

kerl を使って、指定した git リポジトリの diode/beta ブランチから OTP をインストールするわけですね

https://github.com/kerl/kerl

実際、この OTP ではない通常の OTP24 や OTP25 を使ってビルドすると、実行時にエラーが発生します

が、私は kerl ではなく asdf を使っているため、上記の方法は使えません（使いたくありません）

というわけで、 asdf で Erlang を git リポジトリからインストールします

https://github.com/asdf-vm/asdf-erlang#use

こんな感じにリポジトリの URL を環境変数で指定し、ブランチ名を `asdf install` の最後の引数に指定すれば良いようです

```bash
export OTP_GITHUB_URL="https://github.com/diodechain/otp"
asdf install erlang ref:diode/beta
```

これで OK かと思いきや、、、

この問題が発生してしまいました

https://github.com/erlang/otp/issues/5893

macOS の Homebrew で入れていた wxWidgets が 3.2.0 だったことが原因です

```bash
$ brew info wxwidgets
wxwidgets: stable 3.2.0 (bottled), HEAD
Cross-platform C++ GUI toolkit
https://www.wxwidgets.org
/usr/local/Cellar/wxwidgets/3.2.0 (834 files, 25.4MB) *
```

3.1.6 の時点で erlang のビルドに影響のある変更が入っており、
今回入れようとしている OTP ではそれへの対応が入っていないのです

では、 3.1.5 を指定して入れようとすると、、、

```
$ brew install wxwidgets@3.1.5
Warning: No available formula with the name "wxwidgets@3.1.5". Did you mean wxwidgets@3.0?
```

となって、インストールできません

Homebrew のリポジトリー上から、 3.1.5 の情報が消えてしまっているのです

というわけで、 Homebrew のリポジトリーを過去に戻します

まず、 3.1.5 が存在していたコミットを探します

```bash
cd /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
git log --oneline wxwidgets.rb
```

しばらくすると、過去を遡ってちょっとずつ wxWidgets の履歴が出てきます

3.1.5 が出てきた時点で command + C で終了します

```bash
0141b0784c2 wxwidgets: update 3.2.0 bottle.
4b220a83c8d wxwidgets 3.2.0
725e88dc19c wxwidgets: update 3.1.7 bottle.
f02810ab10a wxwidgets 3.1.7
3d8559f4ec1 wxwidgets: update 3.1.6 bottle.
787b6c5eed0 wxwidgets 3.1.6
fc9831dc24b wxwidgets: update 3.1.5 bottle.
b3fddce87df wxwidgets: update 3.1.5 bottle.
```

3.1.5 のコミット番号 (fc9831dc24b) をコピーします

当該コミットをチェックアウトしてきます

```bash
git checkout fc9831dc24b wxwidgets.rb
```

この状態で、自動更新を OFF にして wxWidgets をインストールします
※ `HOMEBREW_NO_AUTO_UPDATE=1` を付けていないと、リポジトリが更新されて結局最新版が入ってしまう

```
HOMEBREW_NO_AUTO_UPDATE=1 brew install wxwidgets
```

これで過去のバージョンが入りました

wxWidgets　の 3.1.5 が入っている状態で、もう一度 Erlang をインストールします

```bash
asdf install erlang ref:diode/beta
```

実行すると、エラー終了はしませんが、途中で以下のようなメッセージが表示されます

```bash
sed: 1: "/^.*git,asdf_diode/beta$/d": undefined label 'eta$/d'
```

確認のため、インストールされているバージョンの一覧を表示してみます

```bash
$ asdf list erlang
  22.3.4.24
  24.3.3
  25.0.3
```

はい、入っていません

asdf のバグで、ブランチ名に `/` が入っていると、正常にインストールできないようです

これを修正するのも骨が折れるので、
リポジトリをフォークしてから新しくブランチ (beta) を作ります

https://github.com/RyoWakabayashi/otp/tree/beta

これで、自分のリポジトリから OTP をインストールします

```bash
export OTP_GITHUB_URL="https://github.com/RyoWakabayashi/otp"
asdf install erlang ref:beta
```

改めてインストールされているバージョンの一覧を表示してみます

```bash
$ asdf list erlang
  22.3.4.24
  24.3.3
  25.0.3
  ref:beta
```

というわけで、ようやくインストールできました、、、

## バージョン指定

xcode 等をインストールしてから、サンプル実装のリポジトリをクローンします

```bash
git clone https://github.com/elixir-desktop/ios-example-app.git
cd ios-example-app
```

asdf で管理している場合、以下のように Elixir と Erlang 、 Node.js のバージョンを指定します
※インストールしていない場合は先にインストールしておきます

```bash
asdf local elixir 1.13.4-otp-24
asdf local erlang ref:beta
asdf local nodejs 12.16.1
```

## 依存パッケージのインストール

Cathage で Swift の依存パッケージ (ZIPFoundation) をインストールします

```bash
carthage update --use-xcframeworks
```

## XCode でプロジェクトを開く

XCode でプロジェクトを開きます

```bash
open todoapp.xcodeproj
```

そうすると、以下のように表示されます

![スクリーンショット 2022-07-27 11.53.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/f0a59476-0ed2-1731-5e90-467d52ee8531.png)

app.zip が赤いのは、ファイルが存在しないためです

Build Phase の Run Scripts を見てみましょう

![スクリーンショット 2022-07-27 11.57.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/580f85b4-90b7-faf6-c086-8206ee4f60c8.png)

こんなスクリプトになっています

```bash
#!/bin/bash
set -e
# Setting up the PATH environment 
eval $(/opt/homebrew/bin/brew shellenv)
# Ensure compilation is using the correct runtime
. ~/projects/24.beta/activate
export PATH="$HOME/elixir/bin:$PATH"
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm

BASE=`pwd`
export MIX_ENV=prod
export MIX_TARGET=ios

if [ ! -d "elixir-app" ]; then
  git clone https://github.com/elixir-desktop/desktop-example-app.git elixir-app
  cd elixir-app
  mix deps.get
else
  cd elixir-app
fi

if [ ! -d "assets/node_modules" ]; then
  cd assets && npm i && cd ..
fi

if [ -f "$BASE/todoapp/app.zip" ]; then
  rm "$BASE/todoapp/app.zip"
fi

mix assets.deploy && \
  mix release --overwrite && \
  cd _build/ios_prod/rel/todo_app && \
  zip -9r "$BASE/todoapp/app.zip" lib/ releases/ --exclude "*.so"
```

ビルドの流れは以下のようになっています

- 使用する言語のバージョンを指定
- Desktop 版の実装コードをクローン
- Elixir の依存パッケージを取得
- JS の依存パッケージを取得
- 古い `app.zip` (前回実行結果)を削除
- Elixir のコードをリリースようにコンパイルして app.zip に圧縮する

app.zip が存在しなかったのは、ビルド時に作成されるものだったからですね

## ビルドの実行とエラー対応

Team と Bundle Identifier を変更します

エミュレータでは動作しないので、 iPhone を接続して Run します

するとエラーが表示されます

 ```
ld: '/Users/oec/Library/Developer/Xcode/DerivedData/todoapp-fpynodswplkqbmfjqesdzwnvefvb/Build/Products/Debug-iphoneos/liberlang.a(erl_init.o)' does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
```

Build Settings -> Build Options -> Enable Bitcode を No に変更します

![スクリーンショット 2022-07-27 12.13.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/1ebf407a-2eb0-3ad0-b775-baaf18a26e08.png)

Run Scripts の中で実行する場合、環境変数を引き継がない関係で、
どうしても正常にビルドできなかったので、プロジェクトのルート直下に以下の内容で `run_mix.sh` を作ります

```bash
#!/bin/zsh
set -e

BASE=`pwd`
export MIX_ENV=prod
export MIX_TARGET=ios

if [ ! -d "elixir-app" ]; then
  git clone https://github.com/elixir-desktop/desktop-example-app.git elixir-app
fi

cd elixir-app

mix clean
mix deps.clean --all
rm -f mix.lock
mix deps.get

if [ ! -d "assets/node_modules" ]; then
  cd assets && npm i && cd ..
fi

if [ -f "$BASE/todoapp/app.zip" ]; then
  rm "$BASE/todoapp/app.zip"
fi

mix assets.deploy && \
  mix release --overwrite && \
  cd _build/ios_prod/rel/todo_app && \
  zip -9r "$BASE/todoapp/app.zip" lib/ releases/ --exclude "*.so"
```

`mix.lock` を消しているのは、最新の Bridge を取得するためです

https://github.com/elixir-desktop/bridge

run_mix.sh にパーミッションを追加してから実行します

```bash
chmod +x run_mix.sh
./run_mix.sh
```

そうすると、 todoapp/app.zip が作成されます

Build Phase の Run Scripts を削除してから、
再び Run します

すると、以下のようなエラーが表示されます

```
'/usr/lib/swift/ZIPFoundation.framework/ZIPFoundation' (no such file),
```

フレームワークが正しくコピーできていないようです

General -> Frameworks, ... から、各フレームワークの `Embed` の値を `Embed & Sign` に変更します

![スクリーンショット 2022-07-27 13.42.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/229826cf-4a7b-74f1-532f-2d5f87cb2f65.png)

また実行すると、次のエラーが表示されます

```
[0m[22m[notice] Application todo_app exited: exited in: TodoApp.start(:normal, [])
    ** (EXIT) an exception was raised:
        ** (ErlangError) Erlang error: :enoent
            (elixir 1.13.4) lib/system.ex:1044: System.cmd("defaults", ["read", "-g", "AppleLocale"], [])
            (desktop 1.4.0) lib/desktop.ex:79: Desktop.language_code/0
            (desktop 1.4.0) lib/desktop.ex:51: Desktop.identify_default_locale/1
            (todo_app 1.0.0) lib/todo_app.ex:23: TodoApp.start/2
            (kernel 8.0) application_master.erl:293: :application_master.start_it_old/4
```

Elixir Desktop の lib/desktop.ex:79 を見てみます

https://github.com/elixir-desktop/desktop/blob/main/lib/desktop.ex#L79

```elixir
    with MacOS <- Desktop.OS.type(),
         {locale, 0} <- System.cmd("defaults", ~w(read -g AppleLocale)),
         [code] <- Regex.run(~r/[a-z]+_[A-Z]+/, locale) do
```

どうやら、 MacOS ではないのに MacOS と判定されてしまっているようです

`Desktop.OS.type()` の定義を見てみます

```elixir
  @spec type :: Linux | MacOS | Windows | Android | IOS
  def type() do
    case System.get_env("ELIXIR_DESKTOP_OS", nil) do
      "android" ->
        Android

      "ios" ->
        IOS

      _ ->
        case :os.type() do
          {:unix, :darwin} -> MacOS
          {:unix, :linux} -> Linux
          {:win32, _} -> Windows
        end
    end
  end
```

環境変数 ELIXIR_DESKTOP_OS が ios なら IOS になるようです

Edit Scheme を開き

![スクリーンショット 2022-07-27 13.52.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/cc0997f3-b4d0-2e7a-7542-dbbce3b670f5.png)

Run スキームの Arguments -> Environment Variables で
Name=ELIXIR_DESKTOP_OS　Value=ios を追加します

![スクリーンショット 2022-07-27 13.51.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/45da2d46-37b7-f1bf-11e6-854645fe98a8.png)

続いてのエラーはこちらです

```
[0m[22m[notice] Application todo_app exited: exited in: TodoApp.start(:normal, [])
    ** (EXIT) an exception was raised:
        ** (File.Error) could not make directory (with -p) "/private/var/mobile/Containers/Data/Application/3A3280A6-4B45-4C5E-A6BE-B7D20B4EF3AF/.config/todo": no such file or directory
            (elixir 1.13.4) lib/file.ex:316: File.mkdir_p!/1
            (todo_app 1.0.0) lib/todo_app.ex:24: TodoApp.start/2
            (kernel 8.0) application_master.erl:293: :application_master.start_it_old/4
```

ディレクトリーの作成に失敗しています

lib/todo_app.ex　の24行目を見てみます

```elixir

  def config_dir() do
    Path.join([Desktop.OS.home(), ".config", "todo"])
  end

  @app Mix.Project.config()[:app]

  def start(:normal, []) do
    Desktop.identify_default_locale(TodoWeb.Gettext)
    File.mkdir_p!(config_dir())
```

ホームディレクトリーの中の .config/todo ディレクトリーを作ろうとしています

しかし、どうやらホームディレクトリーの指定が誤っているようです

`ios-example-app` の todoapp/Bridge.swift を見ると、

https://github.com/elixir-desktop/ios-example-app/blob/main/todoapp/Bridge.swift#L41

```swift
        home = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0].appendingPathComponent("io.elixirdesktop.example")
```

パスの指定が `io.elixirdesktop.example` 固定になってしまっています

これはいけません

ちゃんと設定から取ってくるようにします

```swift
        home = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0].appendingPathComponent(Bundle.main.bundleIdentifier!)
```

これで再実行すると、、、

![IMG_7F61B6421742-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/01647d83-3d82-36a6-7277-e38c1502d77a.jpeg)

やっと動きました！！

最後の Bundle Identifier のところは明確にハードコードで悪いので、 PR を出しておきました

https://github.com/elixir-desktop/ios-example-app/pull/5

## おわりに

これで Elixir でモバイルアプリを作る準備ができました