7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-07-30

Elixirによるデジタライゼーション実装コミュニティ「Elixir Digitalization Implementors」
福岡Elixirコミュニティ「fukuoka.ex」
小倉Elixirコミュニティ「kokura.ex」

のpiacereです

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

前回からのTimexとElixir標準日時モジュール比較の続きで、今回はいよいよ、elixir.jp Slackのスレッドでも問題提起されていた、Timexをescriptで使おうとすると、TimexのdepsであるtzdataのETSテーブルロード問題に引っかかる件について、実際にescriptを使って検証してみようと思います

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

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

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

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

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

https://findy.connpass.com/event/218661
image.png

本コラムの検証環境

本コラムは、以下環境で検証しています(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()を追加します

lib/basic.ex
defmodule Basic do

  def main(_argv \\ []) do
    IO.inspect Timex.now()
  end
end

escript用設定を追加します

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にタイムゾーンとして設定します

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

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

まず、Tzdataが参照している

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

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

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」を渡したらどうなるでしょう?

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のソースコードを見れば一目瞭然です

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」、つまり自身のパスを使う場合は、どうでしょう?

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

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を下記のようにしてみましょう

config/runtime.exs
import Config

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

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

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]

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

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.pngimage.png にて、どうぞ応援よろしくお願いします:bow:

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?