How Elixir Does Project and Dependency Management

Sep 19 2015

I am going to come right out and say it: In less than an hour, Elixir's Mix tool won me over. Elixir seems to have done project and dependency management right.

Elixir is a Ruby-like functional language that runs in the Erlang VM. One of Elixir's more interesting features is its built-in project and package management tool, mix. Over the last several weeks, I have been in many discussions throughout the Go langauge community about what a good package or project manager should look like. This post focuses on how another community implemented theirs.

Elixir's heritage makes its approach to package management fascinating. Based on Erlang's VM, Elixir (like many JVM languages) has been able to leverage a mature library ecosystem. And it has tightly integrated with Erlang's hex.pm package management. But to make the language more usable, the developers have created robust tooling around project and package management that makes it so easy to work with that I can write this article as I learn the system.

The First-Class Project

Most of the languages I typically work with do not include (as part of the core distribution) project management tooling. Elixir provides a single tool, mix, that handles projects, including library dependencies. In this sense, I say that the project is a first-class citizen for the language.

Creating a new project goes like this:

$ mix create jv --module JV

The above creates a new project folder (jv), complete with a bunch of standard files. Since I added the --module JV flag, it also scaffolded a JV module for me in the project's library.

The mix tool includes a number of tools for working with the project, including mix compile, mix run, mix clean, and even a command for packaging the project into an archive, mix archive.build. There's also a convenient tool for starting an Elixir shell inside of the project: iex -S mix.

One fascinating thing about Elixir is that mix-based projects come with three separate environments: test, dev, and prod. These largely impact the way the project is built and executed, giving you the ability to change configurations depending on the target for your build. (For example, the dev environment may enable debug logging by default, while prod may disable it.)

The Mixfile

Among various programming languages, there seem to be different approaches to whether package metadata belongs in code, or in a parseable file format.

Java, Python, PHP, and Node.js (NPM) opted for parseable formats. JSON, XML, and YAML seem to be the most popular formats.

Elixir instead declares a module, called the Mixfile, that declares attributes in code. It is located in a file called mix.exs, which is an Elixir source file.

The mix.exs automatically generated for me by mix looks like this:

defmodule JV.Mixfile do
  use Mix.Project

  def project do
    [app: :jv,
     version: "0.0.1",
     elixir: "~> 1.0",
     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

Roughly, there are three sections to this file:

  • Information about the project itself, and the minimum requirements to run it.
  • Application configuration.
  • External dependencies. (Actually, these are defined in a private function that is called when constructing the main metadata. Conventionally, they're declared as if they were separate.)

Essentially, all of the project's functional metadata (that needed to build and run) is stored in this one file.

Dependency Versioning

By default, mix seems to support three different package sources:

Working with Hex packages is trivial. You simply supply the package name and the version(-ish) that you want to pull.

For example, I would like to install the JSON decoder called poison.

Here's what the dependencies section looks like in our mix.exs file:

  defp deps do
    [{:poison, "~> 1.5"}]
  end

The versioning support in Elixir is brilliantly simple to use:

  • Version numbers are declared in the Mixfile (see version in the example above).
  • The versioning scheme is well-defined:
    • The main version number is SemVer 2.0
    • A pre-release string can be appended (-beta.1)
    • An optional build string can be appended (+20150919)
  • Version resolution allows both exact matches and range matches
    • == 2.1.0 means the dependency MUST be exactly that version
    • > 2.1.0 means the dependency must be greater than 2.1.0, and similar operators like >=, <, and <= are supported
    • Requirments can be conjoined (and) or disjoined (or)
    • And possibly best of all, there is a convenient "fuzzy" declaration: ~> 2.1.0, which can be read as "anything greater than or equal to 2.1.0, as long as it's less than 2.2". Or, simpler, "Any patch release of 2.1".

So in our example above, I requested version 1.5, whatever its latest patch release is.

Dependency Tooling

From here, installing a dependency is trivially easy:

$ mix deps.get
Running dependency resolution
Dependency resolution completed successfully
  poison: v1.5.0
* Getting poison (Hex package)
Checking package (https://s3.amazonaws.com/s3.hex.pm/tarballs/poison-1.5.0.tar)
Fetched package
Unpacked package tarball (/Users/mbutcher/.hex/packages/poison-1.5.0.tar)

Further management options include:

  • mix deps prints a list of all dependencies for the project
  • mix deps.update updates depdencies to their latest (within the version contrstraints).
  • mix deps.compile compiles the declared dependencies

There are also a host of hex specific actions you can take.

While my Elixir code is clearly on the neophyte edge, dependency management seems to be a breeze.

Why Elixir/Mix Is Great

Conversations about project management or dependency management often seem to stumble into pedantic bike-shedding about the differences between project metadata, dependencies, and build tools. Elixir/Mix seems to have foregone the pedantics and just focused on the use cases.

The mix tool is undeniably convenient. It handles all of the common cases for managing Elixir projects. Now, I'm sure there are issues with the tool that established Elixir developers groan about. But the tooling seems to have taken the best parts of existing tools and approaches, and combined them into one consistent and useable toolchain.

The result is something that feels right. And when it comes to the day-in, day-out job of building applications, the utility of the tool is what counts.

Conclusion

Elixir's Mix tool seems to genuinely make project and dependency management straightforward. In a mater of 90 minutes, I went from no knowledge of Mix to having a working and compiling project.

What is the cost of ease of use? Perhaps transparency. I am not at all sure how Mix works behind the scenes, and how hard it would be to manage dependencies without Mix. In fact, because so many of the details are handled for me, I can't say that I even know how Elixir loads packages.

But perhaps that's a small price to pay for having a system that is convenient and powerful.