Environment-specific `import` in (project) `.iex.exs` file?

I want to do something like this in my project’s main .iex.exs file:

import_file_if_available ".iex.#{Mix.env}.exs"

But that doesn’t work because import_file_if_available requires a string literal.

case Mix.env do
  :dev -> import_file_if_available(".iex.dev.exs")
  :test -> import_file_if_available(".iex.test.exs")
  :prod -> import_file_if_available(".iex.prod.exs")
end

should work?

1 Like

That doesn’t work. I believe the problem is that import and all its variants, like import_file_if_available, are lexically scoped, i.e. they’re only effective within the block in which they’re run. In your example, that would be only inside the three case pattern match clauses, but not outside those clauses, so not in the iex session either.

But you gave me an idea:

_ = File.copy ".iex.#{Mix.env}.exs", ".iex.env.exs"
import_file_if_available ".iex.env.exs"

Sadly, that didn’t work either – I’m guessing import_file_if_available is called before the File.copy/2 call executes, which makes sense as I think the former is called at compile time.

I think there might be a way to create the .iex.env.exs file when iex is started, but before it compiles the .iex.exs file.

1 Like

Ok, my supervisor said I had to retry again after my first attempt crashed…

You’re totally right, I only tried it with IO.puts (a side effect) and not with bindings but I think I found a solution after browsing through mix github repo and googling. I’ve tried it in a blank project setup and it seems to work:

# .iex.exs
Code.eval_file(".iex.#{Mix.env()}.exs")

then having this:

#.iex.dev.exs
defmodule Hello do
  def world do
    "Hello World!"
  end
end

Lets me just call Hello.world in the IEx REPL, but not found a way yet to define aliases that then propagate into the REPL. (maybe fiddling with/looking into quick_alias code could make this work)

Please try it in for your case! Hope it helps. :slight_smile:

1 Like

I thought at first that “my supervisor” referred to a person! (I’m still not totally sure still whether it’s an Elixir/OTP/Elixir object or a human.)

That was a good idea. A friend had a similar one:

env_file = ".iex.#{Mix.env}.exs"
if File.exists?(env_file), do: Code.eval_file(env_file)

Unfortunately, Code.eval_file doesn’t work. I’m guessing that’s because it doesn’t modify the current environment (Macro.Env struct, accessible as __ENV__ in iex):

iex()> h Code.eval_file

                    def eval_file(file, relative_to \\ nil)                     

  @spec eval_file(binary(), nil | binary()) :: {term(), binding :: list()}

Evals the given file.

Accepts relative_to as an argument to tell where the file is located.

While require_file/2 and compile_file/2 return the loaded modules and their
bytecode, eval_file/2 simply evaluates the file contents and returns the
evaluation result and its bindings (exactly the same return value as
eval_string/3).

Note that eval_file returns “its bindings”, i.e. those set by import calls in the evaluated file.

Here’s my current – working! – solution:

‘Shadow’ the Mix compile task (in mix.exs):

  defp aliases do
    [
      compile: [&copy_environment_iex_exs_file/1, "compile"],
      ...
    ]
  end

  defp copy_environment_iex_exs_file(_) do
    File.rm ".iex.env.exs"
    File.copy ".iex.#{Mix.env}.exs", ".iex.env.exs"
  end

That should ensure the file .iex.env.exs exists if – and only if – the relevant environment-specific file does.

In .iex.exs:

# This file should be created by `mix compile`:
import_file_if_available ".iex.env.exs"

I also tried creating a macro (a custom sigil) to wrap import_file_if_available:

defmodule MyApp.ImportWithInterpolationSigil do

  import IEx.Helpers, only: [import_file_if_available: 1]

  defmacro sigil_i(t, _modifiers) do
    t_string      = Macro.to_string(t)
    {t_evaled, _} = Code.eval_string(t_string)

    quote do
      import_file_if_available unquote(t_evaled)
    end
  end

end

and then using it in the .iex.exs file like this:

import MyApp.ImportWithInterpolationSigil

~i(".iex.#{Mix.env}.exs")

I checked, by replacing import_file_if_available in the macro with IO.puts and the output identifies the problem:

(
  IO.puts("Evaluating `.iex.dev.exs` ...")
  import(MyApp.SomeModule, only: [foo: 1, foo: 2])
)

The macro is surrounding the contents of my .iex.dev.exs file with parentheses, so the import calls in that file are inside a block and thus don’t affect the iex environment.

After writing the above, it occurred to me that the problem with my macro might be that it’s a sigil. And I was right! A regular macro works just fine:

  defmacro import_with_interpolation(t) do
    t_string      = Macro.to_string(t)
    {t_evaled, _} = Code.eval_string(t_string)

    quote do
      import_file_if_available unquote(t_evaled)
    end
  end

Here’s how it can be called in the .iex.exs file:

import Partially.Test.ImportWithInterpolation

import_with_interpolation ".iex.#{Mix.env}.exs"
3 Likes

I just posted to the Elixir mailing list to propose adding my macro (or updating the existing import_file_if_available macro) to the IEx.Helpers module directly:

1 Like