Note: Elixir v1.4 has been released and improves on many points touched by this article. From v1.4, Elixir will automatically infer the list of applications based on your dependencies. For more information, read the official announcement.
In my journey as a curious Elixir developer, I’ve come across this seemingly simple question a few times: which applications
from third-party libraries do I need to declare in my mix.exs
file?
Before we get down to the nitty-gritty of application dependencies, let’s first recap some basics of the initialization process in Elixir OTP applications.
In its essence, an OTP application is a reusable software component, consisting of multiple modules of its own and it can also depend on third-party code. These dependencies can be either library applications — a collection of standalone modules and functions, with no processes involved — or active applications, with their own life cycles and supervision trees. This distinction is quite subtle, it took me a while to fully grasp its implications.
Applications are defined with an application resource file, like my_app.app
, which is a metadata file comprised of a single Erlang term. It includes all the information needed to start our application and is managed by Mix. If you want to dig deeper into that topic you can take a look at its documentation. Once we create a new application with mix new my_app
a brand new mix.exs
file is generated. This is the file that customizes how my_app.app
will be assembled by Mix and it is the file we’ll be dealing with in the Elixir world. Here is what it looks like:
defmodule MyApp.Mixfile do
use Mix.Project
def project do
[app: :my_app,
version: "0.1.0",
elixir: "~> 1.3",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
deps: deps()]
end
# Configuration for the OTP application
#
# Type "mix help compile.app" for more information
def application do
[applications: [:logger]]
end
# Dependencies can be Hex packages:
#
# {:mydep, "~> 0.3.0"}
#
# Or git/path repositories:
#
# {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
#
# Type "mix help deps" for more examples and options
defp deps do
[]
end
end
As we can see, there are some important pieces of information in it, such as the app name, version, Elixir version requirements and so on. In addition, the application
function lets us explain what is required to boot our application: which other applications need to be started before ours, locally registered processes, which module represents the starting point of our application and also some default values for the application environment. By running mix help compile.app
we can get more information about that function. The docs available for the Application
behavior — which abstracts the initialization process per se — are pretty extensive and helpful as well.
So far, so good. Now things start to get interesting. Let’s take a look at the mix.exs
file of a new Phoenix project. First we generate an application skeleton with mix phoenix.new hello
, then we add {:exrm, "~> 1.0"}
to our deps
, including the Exrm tool for generating our releases.
defmodule Hello.Mixfile do
use Mix.Project
def project do
[app: :hello,
version: "0.0.1",
elixir: "~> 1.3",
# ...
deps: deps()]
end
# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
def application do
[mod: {Hello, []},
applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext,
:phoenix_ecto, :postgrex]]
end
# ...
defp deps do
[{:phoenix, "~> 1.2.0"},
{:phoenix_pubsub, "~> 1.0"},
{:phoenix_ecto, "~> 3.0"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.6"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:gettext, "~> 0.11"},
{:cowboy, "~> 1.0"},
{:exrm, "~> 1.0"}]
end
# ...
end
Along with the :logger
application we’ve seen before in our vanilla Elixir app, now we have a few other applications we depend upon: some from Phoenix itself, an HTTP server, some i18n utilities and a database driver too. If we pay close attention to the deps
and the applications
lists, we can see it is almost a 1 to 1 ratio. Every application
relates to its dep
counterpart, like cowboy
, phoenix_pubsub
, postgrex
and others. The exceptions are :logger
, which is pre-built as part of Elixir, exrm
and phoenix_live_reload
.
That mismatch confused me. I wondered if I wasn’t mixing them up (pun intended).
Library applications must be included too
Why should library-only applications like phoenix_html
and gettext
be included in my application
function then? It’s not that they need to be booted up and start spawning processes or something. It turns out that our application
function has more to do with Releases and runtime than with supervision trees. Yes, listing active applications ensures they are started before our application, but including library applications will also make sure they will be included in our installable release packages.
To support that claim, we can see that even exrm
warns us when we forget to do so. Let’s try removing phoenix_html
and cowboy
from our applications
:
defmodule Hello.Mixfile do
# ...
def application do
[mod: {Hello, []},
applications: [:phoenix, :phoenix_pubsub, :logger, :gettext, :phoenix_ecto, :postgrex]]
end
end
Then make a new release:
mix deps.get
Running dependency resolution
* Getting phoenix (Hex package)
Checking package (https://repo.hex.pm/tarballs/phoenix-1.2.0.tar)
Using locally cached package
...
MIX_ENV=prod mix release
Building release with MIX_ENV=prod.
You have dependencies (direct/transitive) which are not in :applications!
The following apps should be added to :applications in mix.exs:
phoenix_html => phoenix_html is missing from hello
cowboy => cowboy is missing from hello
Continue anyway? Your release may not work as expected if these dependencies are required! [Yn]:
Both dependencies would be missing from the final package :bomb:. We certainly don’t want that.
Development, test, docs and optional dependencies stay out
You might still ask, shouldn’t cowboy
and phoenix_html
be out of my control and get required automatically by phoenix
? That way I’d just need to worry about phoenix
as a single, rich dependency.
Actually, no. From the Phoenix framework standpoint, none of these applications are actually required. Cowboy isn’t a strict runtime dependency; we can freely run our Phoenix app on another compatible web server of our choice. As for Phoenix HTML and even Gettext, they are not strictly required either. We can totally build our Phoenix app without any HTML output or localization at all, consider an API-only app for example. It’s all up to us.
As we can see in Phoenix’s own mix.exs
file, cowboy
is declared as optional: true
and phoenix_html
as only: :test
, gettext
likewise is only: :test
. The reason these dependencies are declared is that if we eventually need them in our app, they must be at least compatible with the Phoenix version currently in use.
That also explains why exrm
and phoenix_live_reload
can be kept out of the applications list. We don’t want phoenix_live_reload
running in production, since there will be no live code reloading. As for exrm
, it is a tool used exclusively to package our application, our business code has no need to know anything about it.
Cool. How come phoenix_live_reload
works in development though? Well, when we start our application — with mix phoenix.start
, for example — the code is available in the load path, but the phoenix_live_reload
application is not started yet; it only starts when we connect to its channel. We can try it out by starting up our little Phoenix app and checking which applications get loaded.
iex -S mix phoenix.start
Erlang/OTP 18 [erts-7.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
[info] Running Hello.Endpoint with Cowboy using http://localhost:4000
Interactive Elixir (1.3.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Application.loaded_applications
[
{:plug, 'A specification and conveniences for composable modules between web applications', '1.1.6'},
{:hex, 'hex', '0.12.1'},
...
]
A list of all the applications currently loaded is returned when we call Application.loaded_applications
. Now, after we make our first request by opening http://localhost:4000
then calling Application.loaded_applications
once again, we can see two new applications on that list: fs
and phoenix_live_reload
.
iex(2)> Application.loaded_applications
[
{:plug, 'A specification and conveniences for composable modules between web applications', '1.1.6'},
{:hex, 'hex', '0.12.1'},
...
{:fs, 'VXZ FS Listener', '0.9.1'},
...
{:phoenix_live_reload, 'Provides live-reload functionality for Phoenix', '1.0.5'}
...
]
Declare your applications, even when you’re not using releases
I am deploying to Heroku and my application requires no release packaging whatsoever, should I still worry about my list of applications? Yes, definitely. Suppose we need to upgrade some dependencies, one of them used to be just a library application but now has become an active application, e.g. a cache library which now has to keep a reaper process to clean up stale data. Once we upgrade the dep
for that undeclared application
, a runtime bug is potentially introduced, given we have no guarantee our new dependencies’ applications will get started properly.
As a library author, you should take extra care
The same goes for libraries, regardless of open sourcing them or not. Remember that runtime application dependencies are transitive. Let’s say an application A
depends on B
, which in turn depends on C
, the application responsible for ensuring C
is started is B
, unless C
is an optional dependency. In that case, the author of B
should document the pluggable nature of that optional dependency appropriately, or even provide code generators if applicable — as Phoenix does with cowboy
.
Elixir tooling is our friend
One of the most noticeable benefits of Elixir is all the tools-support built around it. Not only in runtime, standing on the shoulders of Erlang, but primarily in development and build time. In case you overlooked the announcement of Elixir v1.3 recently released, you should definitely check it out. Among the improvements in dependency tracking are two new built-in mix tasks: app.tree
and deps.tree
. They are priceless timesavers in this routine task of keeping up with our dependencies.
Summing up
So, when we are evaluating these dependency issues individually, it all boils down to a few simple criteria.
For the deps
function in our Mixfile:
- Always add it to
deps
if it is a direct dependency. - Don’t forget the
only: [...]
option if not every environment needs it. - Optional dependencies must be set as
optional: true
. It is always good to document and explain how they are supposed to be included.
For the applications
key in our application
function:
- Add it to
applications
if it is required at runtime in production.
All said and done, where can we go from here? Well, I’d say start at home, review your mix.exs
. Is there anything important missing? Any unused dependency? Then you can start taking a closer look at your external dependencies’ Mixfiles. Are all runtime dependencies properly required? What about their environments? Once you’re there, take your time to contribute, give back. These tiny bits might seem worthless at first, but rest assured, as some clever man used to say, “success is in the details”.