TimexとElixir標準日時②：escriptでTimexを使ってもエラーを出さず、相対パス指定できるか？

[Elixirによるデジタライゼーション実装コミュニティ「Elixir Digitalization Implementors」
福岡Elixirコミュニティ「fukuoka.ex」
小倉Elixirコミュニティ「kokura.ex」](https://fukuokaex.connpass.com/)
のpiacereです

ご覧いただいて、ありがとうございます :bow:

[前回からのTimexとElixir標準日時モジュール比較](https://qiita.com/piacerex/items/6479e1b9ba5a6cef55ce)の続きで、今回はいよいよ、[elixir.jp Slackのスレッド](https://elixirjp.slack.com/archives/CTGEFMJA0/p1627522398039400)でも問題提起されていた、Timexをescriptで使おうとすると、Timexのdepsである[tzdataのETSテーブルロード問題](https://github.com/bitwalker/timex/issues/86)に引っかかる件について、実際にescriptを使って検証してみようと思います

今まで、Timexと日時系Elixir標準モジュールの比較は、数年単位でサボり続けてて、調査にそこそこボリューム出そうなので、何回かに分けて整理します

内容が、面白かったり、役に立ったら、「いいね」よろしくお願いします :wink:

<h3>:shamrock::shamrock::shamrock: ひさびさに主催イベント以外で登壇します :shamrock::shamrock::shamrock:</h3>

来週8/5（木）19時から、ハイスキルエンジニア転職サービス「Findy」の下記イベントで、RubyとElixirについてパネルディスカッションします

私がElixirを始めたきっかけと、Rubyに感じ続けた課題、それと最近、Elixir案件でやたら求人募集してる背景などについて、どこまで話せるかは分かりませんが、可能な限り、情報共有したいと思います

https://findy.connpass.com/event/218661
[![image.png](https://connpass-tokyo.s3.amazonaws.com/thumbs/01/5e/015ea4ba4dca2e56c37b5956d3404e1b.png)](https://findy.connpass.com/event/218661)

# 本コラムの検証環境

本コラムは、以下環境で検証しています（Windows WSL2で実施していますが、LinuxやMacでも動作する想定です）

- Windows 10
- WSL2 + Ubuntu 18.04
- OTP 24.0
- Elixir 1.12.0

環境構築は、前回のコラムで実施済みなので、未だの方は、[前回コラムをご覧になって、一通り動かしておいてください](https://qiita.com/piacerex/items/6479e1b9ba5a6cef55ce)

なおZoneinfoが、Windows WSL2だと動きますが、Windowsネイティブだと動かない（≒OSが管理するタイムゾーンデータベースを利用する関係で）ため、Windowsの方は、WSL2＋Ubuntu 18.04でお試ししてください

https://qiita.com/piacerex/items/6b64dfe70f48c89d8385

# escriptの準備

escriptで呼び出すためのmain()を追加します

```elixir:lib/basic.ex
defmodule Basic do
…
  def main(_argv \\ []) do
    IO.inspect Timex.now()
  end
end
```

escript用設定を追加します

```elixir:mix.exs
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にタイムゾーンとして設定します

下記ファイルは、デフォルトではフォルダ毎、存在しないため、フォルダ作成とファイル作成を行ってください

```elixir:config/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 さんのコラムのやり方を使えば、エラーを出さずに実行できます

https://qiita.com/torifukukaiou/items/422116d6617892ab7050

まず、Tzdataが参照している

```
iex> Tzdata.Util.data_dir
"/path/your_home/basic/_build/dev/lib/tzdata/priv"
```

これを、runtime.exsに設定します

```elixir:config/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」を渡したらどうなるでしょう？

```elixir:config/runtime.exs
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のソースコードを見れば一目瞭然です

```elixir:deps/tzdata/lib/tzdata/util.ex
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」、つまり自身のパスを使う場合は、どうでしょう？

```elixir:config/runtime.exs
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をパス直接指定に戻した後、下記でデバッグしてみます

```elixir:lib/basic.ex
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を下記のようにしてみましょう

```elixir:config/runtime.exs
import Config

config :tzdata, data_dir: String.replace(Application.app_dir(:basic), "basic/basic", "basic/_build/dev/lib/tzdata/priv")
```

下記コードも元に戻します

```elixir:lib/basic.ex
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]
```

タイムゾーン指定も問題無さそうです

```elixir:lib/basic.ex
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標準モジュールの差異確認にいったん戻ります

# p.s.このコラムが、面白かったり、役に立ったら…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/67568d54-6584-ad33-77c7-b3b3afe70287.png) や ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/f5b819ef-d655-ef72-294c-b1a9d2c5b563.png) にて、どうぞ応援よろしくお願いします:bow: