はじめに
Elixir Desktop は Phoenix LiveView を使って、 macOS でも Windows でも iOS でも Android でもネイティブアプリっぽいものが実装できるパッケージです
今回は iOS 版のサンプル実装を私のローカルマシン(macOS)上でビルドしようとして、
色々なエラーに引っ掛かった話を書いていきます
ビルド環境
- macOS Monterey 12.4
- XCode 13.4.1
- asdf 0.10.0
私は色々な案件で色々な言語、ツールをワサワサ使う関係上、
開発言語やツールの実行バージョン管理に asdf を使っています
例えば Elixir なら asdf install elixir 1.13.4-otp-24
というようにするだけで
Elixir の指定バージョンがインストールでき、
asdf local elixir 1.13.4-otp-24
とするだけで、実行ディレクトリ配下では
Elixir の指定バージョンが使われるようになります
同じことが凡そ全ての開発言語でできるので非常に便利です
custom OTP のインストール
実装例のビルド手順は以下のようになっています
Install the custom OTP version on your machine (*see known todos)
Install xcode from the app store.
Install brew, elixir, git, carthage, npm
brew install elixir carthage git npm
fork / git clone this project to your local disk:
git clone https://github.com/elixir-desktop/ios-example-app.git
Build the dependencies:
cd ios-example-app && carthage update --use-xcframeworks
Open the ios-example-app project with xcode
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 をインストールするわけですね
実際、この OTP ではない通常の OTP24 や OTP25 を使ってビルドすると、実行時にエラーが発生します
が、私は kerl ではなく asdf を使っているため、上記の方法は使えません(使いたくありません)
というわけで、 asdf で Erlang を git リポジトリからインストールします
こんな感じにリポジトリの URL を環境変数で指定し、ブランチ名を asdf install
の最後の引数に指定すれば良いようです
export OTP_GITHUB_URL="https://github.com/diodechain/otp"
asdf install erlang ref:diode/beta
これで OK かと思いきや、、、
この問題が発生してしまいました
macOS の Homebrew で入れていた wxWidgets が 3.2.0 だったことが原因です
$ 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 が存在していたコミットを探します
cd /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula
git log --oneline wxwidgets.rb
しばらくすると、過去を遡ってちょっとずつ wxWidgets の履歴が出てきます
3.1.5 が出てきた時点で command + C で終了します
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) をコピーします
当該コミットをチェックアウトしてきます
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 をインストールします
asdf install erlang ref:diode/beta
実行すると、エラー終了はしませんが、途中で以下のようなメッセージが表示されます
sed: 1: "/^.*git,asdf_diode/beta$/d": undefined label 'eta$/d'
確認のため、インストールされているバージョンの一覧を表示してみます
$ asdf list erlang
22.3.4.24
24.3.3
25.0.3
はい、入っていません
asdf のバグで、ブランチ名に /
が入っていると、正常にインストールできないようです
これを修正するのも骨が折れるので、
リポジトリをフォークしてから新しくブランチ (beta) を作ります
これで、自分のリポジトリから OTP をインストールします
export OTP_GITHUB_URL="https://github.com/RyoWakabayashi/otp"
asdf install erlang ref:beta
改めてインストールされているバージョンの一覧を表示してみます
$ asdf list erlang
22.3.4.24
24.3.3
25.0.3
ref:beta
というわけで、ようやくインストールできました、、、
バージョン指定
xcode 等をインストールしてから、サンプル実装のリポジトリをクローンします
git clone https://github.com/elixir-desktop/ios-example-app.git
cd ios-example-app
asdf で管理している場合、以下のように Elixir と Erlang 、 Node.js のバージョンを指定します
※インストールしていない場合は先にインストールしておきます
asdf local elixir 1.13.4-otp-24
asdf local erlang ref:beta
asdf local nodejs 12.16.1
依存パッケージのインストール
Cathage で Swift の依存パッケージ (ZIPFoundation) をインストールします
carthage update --use-xcframeworks
XCode でプロジェクトを開く
XCode でプロジェクトを開きます
open todoapp.xcodeproj
そうすると、以下のように表示されます
app.zip が赤いのは、ファイルが存在しないためです
Build Phase の Run Scripts を見てみましょう
こんなスクリプトになっています
#!/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 に変更します
Run Scripts の中で実行する場合、環境変数を引き継がない関係で、
どうしても正常にビルドできなかったので、プロジェクトのルート直下に以下の内容で run_mix.sh
を作ります
#!/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 を取得するためです
run_mix.sh にパーミッションを追加してから実行します
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
に変更します
また実行すると、次のエラーが表示されます
[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 を見てみます
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()
の定義を見てみます
@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 を開き
Run スキームの Arguments -> Environment Variables で
Name=ELIXIR_DESKTOP_OS Value=ios を追加します
続いてのエラーはこちらです
[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行目を見てみます
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 を見ると、
home = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0].appendingPathComponent("io.elixirdesktop.example")
パスの指定が io.elixirdesktop.example
固定になってしまっています
これはいけません
ちゃんと設定から取ってくるようにします
home = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0].appendingPathComponent(Bundle.main.bundleIdentifier!)
これで再実行すると、、、
やっと動きました!!
最後の Bundle Identifier のところは明確にハードコードで悪いので、 PR を出しておきました
おわりに
これで Elixir でモバイルアプリを作る準備ができました