Escript: can not load argon2_nif - wrong approach for CLI?

I’m trying to create a command line utility for creating administrative users in a back office application.

defmodule Admin.CLI do

  def main(args) do
    handle_command(args)
  end

  def handle_command(["createadmin" | rest]) do
    {opts, _, _} = OptionParser.parse(rest, switches: [username: :string, password: :string],
      aliases: [u: :username, p: :password])

    username = Keyword.get(opts, :username) || raise "You must supply a username"
    password = Keyword.get(opts, :password) || raise "You must supply a password"

    case Admin.create_user(%{"username" => username, "password" => password}) do
      {:ok, _user} ->
        IO.puts("User #{username} created with password #{password}")
      {:error, _} ->
        IO.puts(:stderr, "Error creating user.")
    end
  end

  def handle_command(_) do
    IO.puts(:stderr, "Invalid command.")
  end
end

But this is the output that I receive when running the command:

$ ./admin createadmin --username test --password test
[warn] The on_load function for module Elixir.Argon2.Base returned:
{:function_clause, [{:filename, :join, [{:error, :bad_name}, 'argon2_nif'], [file: 'filename.erl', line: 446]}, {Argon2.Base, :load_nif, 0, [file: 'lib/argon2/base.ex', line: 115]}, {Argon2.Base, :init, 0, [file: 'lib/argon2/base.ex', ...]}, {:code_server, :"-handle_on_load/5-fun-0-", 1, [...]}]}

[error] Process #PID<0.243.0> raised an exception
** (FunctionClauseError) no function clause matching in :filename.join/2
    (stdlib) filename.erl:446: :filename.join({:error, :bad_name}, 'argon2_nif')
    (argon2_elixir) lib/argon2/base.ex:115: Argon2.Base.load_nif/0
    (argon2_elixir) lib/argon2/base.ex:14: Argon2.Base.init/0
    (kernel) code_server.erl:1340: anonymous fn/1 in :code_server.handle_on_load/5
** (UndefinedFunctionError) function Argon2.Base.hash_password/3 is undefined (module Argon2.Base is not available)
    (argon2_elixir) Argon2.Base.hash_password("test", <<158, 41, 41, 255, 119, 126, 67, 223, 90, 157, 199, 83, 101, 28, 67, 206>>, [])
    (argon2_elixir) lib/argon2.ex:140: Argon2.add_hash/2
    (admin) lib/admin/user.ex:30: Admin.User.maybe_add_hash/1
    (admin) lib/admin.ex:14: Admin.create_user/1
    (admin) lib/admin/cli.ex:14: Admin.CLI.handle_command/1
    (elixir) lib/kernel/cli.ex:121: anonymous fn/3 in Kernel.CLI.exec_fun/2

When running with the same input from iex -S mix, it works as expected:

iex(1)> Admin.CLI.handle_command(["createadmin", "--username", "test", "--password", "test"])
User test created with password test
[debug] QUERY OK db=25.1ms decode=0.8ms queue=0.6ms
INSERT INTO "users" ("password_hash","username") VALUES ($1,$2) RETURNING "id" ["$argon2id$v=19$m=131072,t=8,p=4$dXaxDD2S7LG2sAaxfj8u9w$iJw1bEMSwZCPS3jIFZZ4Fzg/5sGf0hSEh8izr1hlyCY", "test"]

Is my approach wrong here? Should the escript just be sending messages to a handler process that’s already running inside the application?

You can not use NIFs in an escript.


edit

Strictly speaking, you can’t use priv_dir in an escript, which most NIFs rely on to ship the shared object which contains the actual NIF.

1 Like

That’s unfortunate. Back to the drawing board!