Running Project Mix Commands from Any Directory

Jake Craige

While adding Credo as supported linter on Hound, I ran into a problem that I hadn’t seen before. I needed to run mix credo in a temporary directory, but soon discovered Mix only searches the current directory for a .mix.exs. What this means is that you can only run mix from the root of your project if you depend on any configuration or dependencies in your mix.exs file. In most cases this isn’t a problem, but for how Hound works, it introduced a challenge.

To use Credo, you add it to your mix.exs file and execute mix credo to run it. When Hound lints a file, it downloads one file at a time into a temporary directory and runs the linter with a shell command, in that directory, on that file.

The challenge here is that to run Credo we need to run mix credo, which depends on a mix.exs, but we don’t have one. We only have the one file.

Problem Solving

One approach to solve this would be to add a mix.exs in the temporary directory with Credo as a dependency before running mix credo. But there’s a huge problem with this, since Hound lints each file individually, we would be compiling Credo for each file of a project. This would be seriously time consuming and expensive.

What we’d like is to install the linter once, and reuse it for every file. This way we only need to compile it once and each file can use the pre-compiled version. This is how all of Hound’s existing linters work.

Following that approach, let’s add a mix.exs to the root of the linter app that looks like so:

defmodule Linters.Mixfile do
  use Mix.Project

  def project do
    [
      app: :linters,
      version: "0.1.0",
      elixir: "~> 1.4",
      build_embedded: Mix.env == :prod,
      start_permanent: Mix.env == :prod,
      deps: deps(),
   ]
  end

  defp deps do
    [{:credo, "~> 0.6"}]
  end
end

After installing our dependencies with mix deps.get, we can execute mix credo to run the linter. Sweet!

Unfortunately, we’re now back to the initial problem, we can run it from the root of the project, but nowhere else. If you cd into any directory that doesn’t contain this mix.exs and try to run mix credo, it will fail like this:

$ mix credo
** (Mix) The task "credo" could not be found

Next, we need to figure out how to make that work. After a bit of “sleuthing”, a.k.a googling, I came across an environment variable that looked like it would solve this problem, MIX_EXS. From the docs:

MIX_EXS - changes the full path to the mix.exs file

Since we know we need the mix.exs to load our dependencies, let’s try using this. Assuming we’re in a directory called linters and we navigate to its parent via cd ... We’ll run it like so:

$ MIX_EXS=linters/mix.exs mix credo
Unchecked dependencies for environment dev:
* credo (Hex package)
  the dependency is not available, run "mix deps.get"
** (Mix) Can't continue due to errors on dependencies

Seems like progress! This says the credo dependency isn’t available, which is unexpected since we’ve already compiled it when we installed it above. It tells us to run mix deps.get, so let’s try it.

$ MIX_EXS=linters/mix.exs mix deps.get
Running dependency resolution...
Dependency resolution completed:
  bunt 0.1.6
  credo 0.5.3
* Getting credo (Hex package)
  Checking package (https://repo.hex.pm/tarballs/credo-0.5.3.tar)
  Using locally cached package
* Getting bunt (Hex package)
  Checking package (https://repo.hex.pm/tarballs/bunt-0.1.6.tar)
  Using locally cached package

$ MIX_EXS=linters/mix.exs mix credo
==> bunt
Compiling 2 files (.ex)
Generated bunt app
==> credo
Compiling 122 files (.ex)
Generated credo app
No files found!

Please report incorrect results: https://github.com/rrrene/credo/issues

Analysis took 0.00 seconds (0.00s to load, 0.00s running checks)
0 mods/funs, found no issues.

Use `--strict` to show all issues, `--help` for options.

Successful Credo run (Yay!), but an unexpected compilation since we’ve already compiled it in the linters directory. This is exactly what we were trying to avoid in the first place. Turns out this is compiling them into our current directory. If we run ls we’ll see that it has created a few files and directories, _build, deps and mix.lock.

I have to admit, this one took quite a while to figure out what was going on and how to get past it. Lots of “sleuthing” occurred. Eventually I discovered that these were all configurable in the mix.exs file from this documentation.

That docs tell us that we can configure the :deps_path and :lockfile but don’t mention how to change the build path. This is one of those places where open source is really awesome. I was able to discover it is configurable through an aptly named :build_path key in the source.

Now that we can configure it, let’s do so. The problem with the defaults is that they assume a relative path from where we’re running mix, and that’s not true for our use. So let’s update the project config to use absolute ones:

  def project do
    [
      app: :linters,
      version: "0.1.0",
      elixir: "~> 1.4",
      build_embedded: Mix.env == :prod,
      start_permanent: Mix.env == :prod,
      deps: deps(),
      lockfile: Path.expand("mix.lock", __DIR__),
      deps_path: Path.expand("deps", __DIR__),
      build_path: Path.expand("_build", __DIR__),
   ]
  end

Now, after removing the build artifacts from our previous attempt,_build, deps and mix.lock, we can try running the Credo again:

$ MIX_EXS=linters/mix.exs mix credo
No files found!

Please report incorrect results: https://github.com/rrrene/credo/issues

Analysis took 0.03 seconds (0.00s to load, 0.03s running checks)
0 mods/funs, found no issues.

Use `--strict` to show all issues, `--help` for options.

There we go! We can now run mix credo from anywhere on our system as long as we specify the MIX_EXS environment variable with the path to the project that has it compiled.