This post started out its life as a short example of bubble sort in elixir. But I encountered an interesting message when I tried to run my script with mix run bubble.exs
** (Mix) Cannot execute "mix run" without a Mix.Project, please ensure you are running Mix in a directory with a mix.exs file or pass the --no-mix-exs flag
In this case since the file is a true one page script with no dependencies, I could have run elixir bubble.exs
But this error message caught my attention and led to some bigger questions. How does this flag work? How does Mix work?
Mix Tasks
In this example we are using mix run
so I thought I would try to track down the run command in the mix source code. Mix is a sub application within the Elixir source code. So I went to the Elixir repo and tried a search for "def run"
.
Repo: https://github.com/elixir-lang/elixir/tree/master/lib/mix/lib
After poking around for a while I got lucky and found the module Mix.Tasks.Run. It appears that the "run" command is a Task within Mix. This fits with the high level overview of mix provided in the top level Mix module file.
Basically each of the default commands that you can provide to Mix in the command line is a separate task. Each task has a corresponding module with a "run" method that is called.
Tracing a Mix Command
Now that we know Mix is passing our run command to the Mix.Tasks.Run module, lets dig in.
The file starts with a catch all run method:
def run(args) do
{opts, head} =
OptionParser.parse_head!(
args,
aliases: [r: :require, p: :parallel, e: :eval, c: :config],
strict: [
parallel: :boolean,
require: :keep,
eval: :keep,
config: :keep,
mix_exs: :boolean,
halt: :boolean,
...
]
)
run(args, opts, head, &Code.eval_string/1, &Code.require_file/1)
unless Keyword.get(opts, :halt, true), do: System.no_halt(true)
Mix.Task.reenable("run")
:ok
end
This method parses the command line options and passes the parsed data to a run method with more parameters. Interesting that the --no-halt option is handled at this level.
Also deep in the OptionParser file, I found this line interesting.
defp tag_option("no-" <> option = original, config) do
The no-
syntax is used as a switch for options. I always assumed that --halt
and --no-halt
would just be identified as separate strings
Next up we have the run method with expanded parameters.
def run(args, opts, head, expr_evaluator, file_evaluator) do
First it does some more processing of the command line args, then we reach this crucial code block.
cond do
Mix.Project.get() ->
Mix.Task.run("app.start", args)
"--no-mix-exs" in args ->
:ok
true ->
Mix.raise(
"Cannot execute \"mix run\" without a Mix.Project, " <>
"please ensure you are running Mix in a directory with a mix.exs file " <>
"or pass the --no-mix-exs option"
)
end
Here we can see exactly where the --no-mix-exs
option comes into play.
First it checks if we are in a normal Mix project directory with Mix.Project.get()
. If that returns false it checks the args for --no-mix-exs
. Finally it will throw the error we saw at the start. Mystery Solved!
Going Deeper
Mix.Project.get()
It took me forever to track down how this thing works!
When the Mix app is first invoked the Mix.CLI module tries to compile the mix.exs
file in the current directory. By default that file will have the line use Mix.Project
. Once the file is compiled it runs this post compile hook in Mix.Project
def __after_compile__(env, _binary) do
push(env.module, env.file)
end
This hook pushes the env.module name onto an internal stack that is checked in the Mix.Project.get()
command.
Mix.Task.run("app.start", args)
If a mix.exs file is available in the current directory, the run function moves onto the line Mix.Task.run("app.start", args)
. This starts a new task, as you may guess the task is in the Mix.Tasks.App.Start module.
This module starts in its run method and progresses down several functions until it reaches the line case Application.start(app, type) do
This passes the app down to the erlang call :application.start(app, type)
to start the app.
Back to Mix.Tasks.Run.run
After checking if we are in a project directory, this function still has a few more tricks up its sleeve.
process_load(opts, expr_evaluator)
if file do
if File.regular?(file) do
file_evaluator.(file)
else
Mix.raise("No such file: #{file}")
end
end
I wanted to include this because I think it is especially neat. If you run a command like mix run -e "IO.inspect 123"
Mix will run the inline code. This happens in the process_load
function. It checks to see if the -e or eval opts were provided and then uses the Code.eval_string/1 function to run the code on the fly.
Finally Mix will run any individual file given in the command line using the &Code.require_file/1 function.
Wrap Up
Thanks for making it this far. It was a challenge to track down exactly what was going on in the Mix app, but it was well worth the effort. Mix is probably the most used Elixir app there is!
Now that you know all about Mix Tasks, maybe you will be inspired to write your own? https://dev.to/drumusician/things-you-could-do-with-mix-2ni3
Coding in Elixir is fun!
Top comments (0)