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 themix.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.