ご覧いただいて、ありがとうございます
前回からのTimexとElixir標準日時モジュール比較の続きで、今回はいよいよ、elixir.jp Slackのスレッドでも問題提起されていた、Timexをescriptで使おうとすると、TimexのdepsであるtzdataのETSテーブルロード問題に引っかかる件について、実際にescriptを使って検証してみようと思います
今まで、Timexと日時系Elixir標準モジュールの比較は、数年単位でサボり続けてて、調査にそこそこボリューム出そうなので、何回かに分けて整理します
内容が、面白かったり、役に立ったら、「いいね」よろしくお願いします


ひさびさに主催イベント以外で登壇します 

来週8/5(木)19時から、ハイスキルエンジニア転職サービス「Findy」の下記イベントで、RubyとElixirについてパネルディスカッションします
私がElixirを始めたきっかけと、Rubyに感じ続けた課題、それと最近、Elixir案件でやたら求人募集してる背景などについて、どこまで話せるかは分かりませんが、可能な限り、情報共有したいと思います
https://findy.connpass.com/event/218661
本コラムの検証環境
本コラムは、以下環境で検証しています(Windows WSL2で実施していますが、LinuxやMacでも動作する想定です)
- Windows 10
- WSL2 + Ubuntu 18.04
- OTP 24.0
- Elixir 1.12.0
環境構築は、前回のコラムで実施済みなので、未だの方は、前回コラムをご覧になって、一通り動かしておいてください
なおZoneinfoが、Windows WSL2だと動きますが、Windowsネイティブだと動かない(≒OSが管理するタイムゾーンデータベースを利用する関係で)ため、Windowsの方は、WSL2+Ubuntu 18.04でお試ししてください
escriptの準備
escriptで呼び出すためのmain()を追加します
defmodule Basic do
…
def main(_argv \\ []) do
IO.inspect Timex.now()
end
end
escript用設定を追加します
defmodule Basic.MixProject do
use Mix.Project
def project do
[
escript: [main_module: Basic],
…
Tzdataでエラーが出るescript
ビルドし、実行すると、下記のようにエラーが出てしまいます
mix escript.build; mix escript.install; ./basic
…
ERROR! Could not start application tzdata: exited in: Tzdata.App.start(:normal, [])
** (EXIT) an exception was raised:
** (MatchError) no match of right hand side value: {:error, {:shutdown, {:failed_to_start_child, Tzdata.EtsHolder, {%ArgumentError{message: "unknown application: :tzdata"}, [{Application, :app_dir, 1, [file: 'lib/application.ex', line: 880]}, {Application, :app_dir, 2, [file: 'lib/application.ex', line: 907]}, {Tzdata.EtsHolder, :release_dir, 0, [file: 'lib/tzdata/ets_holder.ex', line: 123]}, {Tzdata.EtsHolder, :make_sure_a_release_dir_exists, 0, [file: 'lib/tzdata/ets_holder.ex', line: 101]}, {Tzdata.EtsHolder, :make_sure_a_release_is_on_file, 0, [file: 'lib/tzdata/ets_holder.ex', line: 77]}, {Tzdata.EtsHolder, :init, 1, [file: 'lib/tzdata/ets_holder.ex', line: 16]}, {:gen_server, :init_it, 2, [file: 'gen_server.erl', line: 423]}, {:gen_server, :init_it, 6, [file: 'gen_server.erl', line: 390]}]}}}}
(tzdata 1.1.0) lib/tzdata/tzdata_app.ex:13: Tzdata.App.start/2
(kernel 8.0.1) application_master.erl:293: :application_master.start_it_old/4
デフォルトタイムゾーンをZoneinfoに設定…ダメでした
Timexが、tzdataを使わないよう、runtime.exsにタイムゾーンとして設定します
下記ファイルは、デフォルトではフォルダ毎、存在しないため、フォルダ作成とファイル作成を行ってください
import Config
config :elixir, time_zone_database: Zoneinfo.TimeZoneDatabase
ビルドし直し、実行してみます(runtime.exsなので不要かもですが)
mix escript.build; mix escript.install; ./basic
…
ERROR! Could not start application tzdata: exited in: Tzdata.App.start(:normal, [])
** (EXIT) an exception was raised:
** (MatchError) no match of right hand side value: {:error, {:shutdown, {:failed_to_start_child, Tzdata.EtsHolder, {%ArgumentError{message: "unknown application: :tzdata"}, [{Application, :app_dir, 1, [file: 'lib/application.ex', line: 880]}, {Application, :app_dir, 2, [file: 'lib/application.ex', line: 907]}, {Tzdata.EtsHolder, :release_dir, 0, [file: 'lib/tzdata/ets_holder.ex', line: 123]}, {Tzdata.EtsHolder, :make_sure_a_release_dir_exists, 0, [file: 'lib/tzdata/ets_holder.ex', line: 101]}, {Tzdata.EtsHolder, :make_sure_a_release_is_on_file, 0, [file: 'lib/tzdata/ets_holder.ex', line: 77]}, {Tzdata.EtsHolder, :init, 1, [file: 'lib/tzdata/ets_holder.ex', line: 16]}, {:gen_server, :init_it, 2, [file: 'gen_server.erl', line: 423]}, {:gen_server, :init_it, 6, [file: 'gen_server.erl', line: 390]}]}}}}
(tzdata 1.1.0) lib/tzdata/tzdata_app.ex:13: Tzdata.App.start/2
(kernel 8.0.1) application_master.erl:293: :application_master.start_it_old/4
うーん、ダメですねぇ …
どうも、「Tzdata.EtsHolder」の「:release_dir」が特定できておらずコケているようです
ちなみに、runtime.exsでは無く、コンパイルタイムで固定するconfig.exsでも試してみましたが、ダメでした
Configでdata_dirを指定すれば成功するが…
@torifukukaiou さんのコラムのやり方を使えば、エラーを出さずに実行できます
まず、Tzdataが参照している
iex> Tzdata.Util.data_dir
"/path/your_home/basic/_build/dev/lib/tzdata/priv"
これを、runtime.exsに設定します
import Config
config :tzdata, data_dir: "/path/your_home/basic/_build/dev/lib/tzdata/priv"
これで動くようになります
mix escript.build; mix escript.install; ./basic
~U[2021-07-30 17:21:06.163565Z]
しかし、絶対パスを指定しなければならないのは、相当イマイチです
ファイルパスを動的に取得するのはどうか?
では、runtime.exsの指定時に、「Tzdata.Util.data_dir」を渡したらどうなるでしょう?
import Config
config :tzdata, data_dir: Tzdata.Util.data_dir
下記のように、「unknown application: :tzdata」と、runtime.exsの中では、「:tzdata」で特定できないようです
mix escript.build; mix escript.install; ./basic
escript: exception error: #{'__exception__' => true,
'__struct__' => 'Elixir.ArgumentError',
message => <<"unknown application: :tzdata">>}
in function 'Elixir.Application':app_dir/1 (lib/application.ex, line 880)
in call from 'Elixir.Application':app_dir/2 (lib/application.ex, line 907)
よくよく見てみると、先ほどのエラー内容にも、同様に「unknown application: :tzdata」が含まれているため、恐らく、escript起動時に「:tzdata」では特定できないのだと思われます
それは、下記Tzdataのソースコードを見れば一目瞭然です
defmodule Tzdata.Util do
…
517: def data_dir do
518: case Application.fetch_env(:tzdata, :data_dir) do
519: {:ok, nil} -> Application.app_dir(:tzdata, "priv")
520: {:ok, dir} -> dir
521: _ -> Application.app_dir(:tzdata, "priv")
522: end
523: end
…
:tzdataがダメなら、アプリ自身でならどうか?
それでは、「:basic」、つまり自身のパスを使う場合は、どうでしょう?
import Config
config :tzdata, data_dir: String.replace(Application.app_dir(:basic), "lib/basic", "lib/tzdata/priv")
この場合、先ほどの「Tzdata.Util.data_dir」を渡した際のエラーは出ませんが、その前のエラーに戻っており、パス指定が上手くいっていないように見えます
一体、escriptの中で、「Application.app_dir(:basic)」は、どんな値を返しているのでしょう?
runtime.exsをパス直接指定に戻した後、下記でデバッグしてみます
defmodule Basic do
…
def main(_argv \\ []) do
# IO.inspect Timex.now()
IO.inspect Application.app_dir(:basic)
end
end
えぇぇ!? … なんだかmixで実行したときと、まるで異なるヘンテコな値が返ってきているようです …
mix escript.build; mix escript.install; ./basic
"/path/your_home/basic/basic"
basic PJ直下のbasic escriptがパスってことですかね
では、この結果を踏まえて、runtime.exsを下記のようにしてみましょう
import Config
config :tzdata, data_dir: String.replace(Application.app_dir(:basic), "basic/basic", "basic/_build/dev/lib/tzdata/priv")
下記コードも元に戻します
defmodule Basic do
…
def main(_argv \\ []) do
IO.inspect Timex.now()
end
end
これで、とりあえず動くようになりました
mix escript.build; mix escript.install; ./basic
~U[2021-07-30 17:21:06.163565Z]
タイムゾーン指定も問題無さそうです
defmodule Basic do
…
def main(_argv \\ []) do
IO.inspect Timex.now("Japan")
end
end
mix escript.build; mix escript.install; ./basic
# DateTime<2021-07-31 03:06:23.183732+09:00 JST Japan>
このパス指定は、「_build/dev」とか直接指定してて、釈然としないものはあるものの、一応、動的パスによる相対指定は叶いました
そもそも、escriptは、mix環境下で無いから挙動が異なるのは、仕方無いと言えば、仕方無いのでしょう
なお後学までに、escript内では、「Mix.Project.app_path()」のような、mix系モジュールも一切使えず、指定すると、undefinedとなってしまいます
最後に
結局、Tzdataのパス問題を解決しないことには、Timexは、Zoneinfoでどうにかなるものでは無かった … という結論でした
Tzdataのパス指定は、ギリギリ動的に相対パス指定ができて、絶対パス指定は避けられたものの、イマイチ感は漂っています
そういえば、モジュールのロード順とかを指定する技や、escript配下に任意のモジュールをロードする技とか、あったような … それを使えば、キレイに出来るのかも?
でも、割と試行錯誤して疲れたんで、次回は、いったんescriptを離れて、TimexとElixir標準モジュールの差異確認にいったん戻ります