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:- Hex, the mature "package manager for the Erlang ecosystem".
- GitHub.
- Any other git repository that is accessible by a Git URL.
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 projectmix 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.
from http://technosophos.com/2015/09/19/how-elixir-does-project-and-package-management.html