silbermm / exdoc_cli

CLI for displaying exdoc
https://hexdocs.pm/exdoc_cli/
GNU General Public License v3.0
15 stars 1 forks source link

--open doesn't work with Elixir modules, only erlang modules #11

Open silbermm opened 2 years ago

silbermm commented 2 years ago

I think this has something to do with the escript build, because it works fine inside of iex.

I've tried a few different solutions:

In the end, none of those worked.

The error I get is:

** (ArgumentError) unknown application: :elixir
    (elixir 1.13.1) lib/application.ex:907: Application.app_dir/1
    (elixir 1.13.1) lib/application.ex:934: Application.app_dir/2
    (iex 1.13.1) lib/iex/introspection.ex:170: IEx.Introspection.open_mfa/3
    (iex 1.13.1) lib/iex/introspection.ex:88: IEx.Introspection.open/1
    (elixir 1.13.1) lib/kernel/cli.ex:126: anonymous fn/3 in Kernel.CLI.exec_fun/2
ghost commented 2 years ago

The question is: Why does it work with Erlang module?

Digging into the source code of IEx.Helpers.open/1, We find that the problematic function is:

@elixir_apps ~w(eex elixir ex_unit iex logger mix)a
  @otp_apps ~w(kernel stdlib)a
  @apps @elixir_apps ++ @otp_apps

  defp rewrite_source(module, source) do
    case :application.get_application(module) do
      {:ok, app} when app in @apps ->
        Application.app_dir(app, rewrite_source(source))

      _ ->
        beam_path = :code.which(module)

        if is_list(beam_path) and List.starts_with?(beam_path, :code.root_dir()) do
          app_vsn = beam_path |> Path.dirname() |> Path.dirname() |> Path.basename()
          Path.join([:code.root_dir(), "lib", app_vsn, rewrite_source(source)])
        else
          List.to_string(source)
        end
    end
  end

Application.app_dir is failing, When the alternate path with :code.which works.

Well if :code.which works, what does it return for both of our cases?

I modified the source code of help_command.ex:

def process(%{topic: <<":" <> erlang_module>>, open: open}) do
  IO.puts(:code.which(:"#{erlang_module}"))

And:

def process(%{topic: topic, open: open}) do
  IO.puts(:code.which(:"Elixir.#{topic}"))

Recompiling and reinstalling the escript, I ran:

$ exdoc Enum --open
/usr/home/rowland/.asdf/install/elixir/1.13.3-otp-24/.mix/escripts/exdoc/Elixir.Enum.beam
<error message>
$ exdoc :lists --open
/home/rowland/.asdf/installs/erlang/24.3.2/lib/stdlib-3.17.1/ebin/lists.beam

Note that in the first case, Elixir is bundled into the escript itself, But in the second case, it's using my local installation of Erlang. Since rewrite_source/2 uses the directory it finds, Even if we switched Elixir to use the :code.which result, It wouldn't find the .ex files we need.

This makes sense when we remember that escripts produce an executable that can be run on any system with Erlang installed. Escript pre-supposes that a system has Erlang on it, and doesn't bundle the code inside the escript. Opting instead to use the local installation, wherever it is.

A potential solution: Since we're using mix to install the escript, We can presuppose that the machine has Elixir installed, After all it wouldn't be able to compile the script otherwise.

Looking again at rewrite_source:

@elixir_apps ~w(eex elixir ex_unit iex logger mix)a
  @otp_apps ~w(kernel stdlib)a
  @apps @elixir_apps ++ @otp_apps

 defp rewrite_source(module, source) do
    case :application.get_application(module) do
      {:ok, app} when app in @apps ->
        Application.app_dir(app, rewrite_source(source))

There are only 8 different apps that we need directories for, We could compute those 8 paths at compilation, Or have the user set an environment variable, such as ELIXIR_SOURCE_PATH And write a custom open function to use those paths.