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