Mix Projects
Every page so far was one file, run with elixir file.exs. Real programs have
many modules, tests, settings that differ between your laptop and production,
and code written by other people. Mix, the build tool that ships with Elixir,
handles all of that: it creates projects, compiles them, runs them and their
tests, formats the code and fetches dependencies.
On this page:
- What
mix newgenerates. - A small library,
bill, that adds up and splits a restaurant bill. It depends on a package from Hex, the package registry. - Compile, run, test and format it.
- Build environments (
MIX_ENV) and configuration files. - Scripts that need a package, with
Mix.install/1.
A new project
mix new <name> creates a project directory. We make one in a throwaway
directory just to see what it contains, then delete it.
$ mix new demo
$ rm -rf demo
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/demo.ex
* creating test
* creating test/test_helper.exs
* creating test/demo_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd demo
mix test
Run "mix help" for more commands.
Every Mix project has the same layout: mix.exs describes the project, lib/
holds the code, test/ the tests, and .formatter.exs tells mix format
which files to format. Mix generates two more directories when it runs:
deps/ for downloaded dependencies and _build/ for compiled code. Neither
belongs in version control, which is what the generated .gitignore says.
The project
bill is laid out the same way. mix.exs is an ordinary module. use Mix.Project makes it the project definition, and Mix calls its functions:
project/0names the app (:bill), gives its version, the Elixir versions it works with, and its dependencies.elixirc_options: [warnings_as_errors: true]makes every compiler warning fail the build, so none slip through. (mix compile --warnings-as-errorsdoes the same for a single run.)application/0describes the OTP application the project compiles to: a named component that is loaded, started and stopped as a unit.extra_applications: [:logger]says Elixir'sLoggermust be started beforebill. Applications and Configuration shows what else goes here once you have processes to start.
defmodule Bill.MixProject do
use Mix.Project
def project do
[
app: :bill,
version: "0.1.0",
elixir: "~> 1.20",
elixirc_options: [warnings_as_errors: true],
deps: deps()
]
end
def application do
[extra_applications: [:logger]]
end
defp deps do
[{:decimal, "~> 3.2"}]
end
end
{:decimal, "~> 3.2"} is a dependency on the
Decimal package from
Hex, the package registry for Elixir and Erlang. Floats
can't represent most decimal fractions exactly (0.1 + 0.2 is
0.30000000000000004), so money should never be a float. Decimal does exact
decimal arithmetic.
The string after the name is a version requirement. ~> 3.2 means "3.2 or
any later 3.x": updates that by convention add features without breaking
anything, but never 4.0. With a third number, ~> 3.2.0 would allow only
3.2.x bug fixes. Elixir's Version module does the matching, and shows
exactly what each form accepts:
$ elixir -e 'IO.inspect(for v <- ["3.2.0", "3.9.1", "4.0.0"], do: {v, Version.match?(v, "~> 3.2")})'
$ elixir -e 'IO.inspect(for v <- ["3.2.7", "3.3.0"], do: {v, Version.match?(v, "~> 3.2.0")})'
[{"3.2.0", true}, {"3.9.1", true}, {"4.0.0", false}]
[{"3.2.7", true}, {"3.3.0", false}]
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
/_build/
/deps/
The library itself: three public functions, with exact arithmetic delegated to
Decimal. split/2 works in whole cents so the shares always add up to the
total. format/1 reads the currency from the configuration (below).
defmodule Bill do
@moduledoc "Adds up a bill and splits it, in exact decimal arithmetic."
@doc """
Adds up prices given as strings.
iex> Bill.total(["0.10", "0.20"])
Decimal.new("0.30")
"""
@spec total([String.t()]) :: Decimal.t()
def total(prices) do
Enum.reduce(prices, Decimal.new(0), &Decimal.add(Decimal.new(&1), &2))
end
@doc """
Splits the total between `people`, to the cent. Shares differ by at most
one cent and always add up to the total.
"""
@spec split([String.t()], pos_integer()) :: [Decimal.t()]
def split(prices, people) when is_integer(people) and people > 0 do
cents = prices |> total() |> Decimal.mult(100) |> Decimal.to_integer()
share = div(cents, people)
extra = rem(cents, people)
for n <- 1..people do
Decimal.new(1, if(n <= extra, do: share + 1, else: share), -2)
end
end
@doc "Formats an amount in the configured currency."
@spec format(Decimal.t()) :: String.t()
def format(amount) do
Decimal.to_string(amount) <> " " <> Application.fetch_env!(:bill, :currency)
end
end
Tests live in test/ and use ExUnit, Elixir's test framework.
test_helper.exs runs first and starts it.
ExUnit.start()
use ExUnit.Case turns the module into a test case. Each test is a named
block of asserts: an assert on a comparison that fails shows both sides.
doctest Bill also runs the iex> example in Bill.total/1's @doc as a
test, so documentation examples can't go stale.
defmodule BillTest do
use ExUnit.Case
doctest Bill
test "shares add up to the total" do
shares = Bill.split(["10.00"], 3)
assert Enum.map(shares, &Decimal.to_string/1) == ["3.34", "3.33", "3.33"]
assert Decimal.equal?(Enum.reduce(shares, &Decimal.add/2), Decimal.new("10.00"))
end
test "formats in the test currency" do
assert Bill.format(Decimal.new("1.50")) == "1.50 XTS"
end
end
Fetch dependencies and compile
mix deps.get downloads the dependencies into deps/ and records the exact
versions in mix.lock. It prints download progress and timings, so its
output is hidden here.
$ mix deps.get > /dev/null
$ ls deps
decimal
mix.lock pins every dependency, direct or not, to one version and a
checksum of its contents. Commit it: everyone who builds the project, and
every server, then gets exactly the same code, even after Decimal publishes
3.3. mix deps.update decimal moves the lock forward when you choose to.
%{
"decimal": {:hex, :decimal, "3.2.0", "f1235f05d841a211fda78a08fdb9777f2ff51a71f18455233fae186ff76187c6", [:mix], [], "hexpm", "190bca80580ce61aa69bb0eacadd7677710205c770225c2c2c07e32a5c31b234"},
}
mix compile compiles the dependencies first, then the project, and writes
one .beam file per module (see Compilation) into
_build.
$ mix compile
$ ls _build/dev/lib
$ ls _build/dev/lib/bill/ebin
==> decimal
Compiling 4 files (.ex)
Generated decimal app
==> bill
Compiling 1 file (.ex)
Generated bill app
bill
decimal
Elixir.Bill.beam
bill.app
Besides the module, ebin holds bill.app: the application description Mix
generated from mix.exs. Applications and Configuration
looks inside it.
mix deps.tree shows which package needs which. Here the tree is short.
$ mix deps.tree
bill
└── decimal ~> 3.2 (Hex package)
Run it
mix run -e compiles the project if needed, starts it, and runs one
expression with all of its modules and dependencies available. (mix run file.exs runs a script the same way.)
$ mix run -e 'IO.inspect(Bill.total(["1.10", "2.20"]))'
$ mix run -e 'IO.inspect(Bill.split(["10.00"], 3))'
$ mix run -e 'IO.puts(Bill.format(Bill.total(["1.10", "2.20"])))'
Decimal.new("3.30")
[Decimal.new("3.34"), Decimal.new("3.33"), Decimal.new("3.33")]
3.30 EUR
To try things by hand, iex -S mix starts IEx, Elixir's interactive shell,
inside the project: everything mix run loads is there, and recompile()
picks up the edits you make while it runs. It's interactive, so it can't run
on this page.
Test it
mix test runs every *_test.exs file under test/. Tests run in a random
order to flush out tests that depend on each other; the seed is printed so a
failing order can be repeated with --seed. We fix the seed, pin
--max-cases (how many tests may run at once; the default, twice the number
of CPU cores, is printed too), and drop the line with the run time, so the
output below is the same on every machine. mix test builds in the test
environment (more below), so it compiles everything again first. Each . is a
passing test. Testing with ExUnit covers tests in more depth.
$ mix test --seed 0 --max-cases 1 | grep -v '^Finished in'
==> decimal
Compiling 4 files (.ex)
Generated decimal app
==> bill
Compiling 1 file (.ex)
Generated bill app
Running ExUnit with seed: 0, max_cases: 1
...
Result: 3 passed (1 doctest, 2 tests)
Format it
mix format rewrites the files listed in .formatter.exs in the one standard
Elixir style. With --check-formatted it changes nothing and fails if any
file would change, which is what CI runs. Silence means every file is
formatted.
$ mix format --check-formatted
Given - it formats standard input instead, which shows what it does:
$ printf 'defmodule Tip do\ndef of(amount),do: amount\nend\n' | mix format -
defmodule Tip do
def of(amount), do: amount
end
mix help lists every task (built-in ones, those of your dependencies and
your own), and mix help <task> documents one. --search filters the list:
$ mix help --search deps
mix deps # Lists dependencies and their status
mix deps.clean # Deletes the given dependencies' files
mix deps.compile # Compiles dependencies
mix deps.get # Fetches unavailable and out of date dependencies
mix deps.tree # Prints the dependency tree
mix deps.unlock # Unlocks the given dependencies
mix deps.update # Updates the given dependencies
Environments and configuration
Mix builds in one of three environments, chosen by the MIX_ENV variable:
dev (the default), test (which mix test switches to by itself) and
prod. Each compiles into its own directory under _build, which is why
there was a _build/dev above.
Configuration goes in config/config.exs. Each config :app, key: value sets
a key in that application's environment, a keyword store that belongs to
the OTP application. import Config makes the config macro available, and
the last line pulls in the file for the current environment, so it can
override what came before.
import Config
config :bill, currency: "EUR"
# Last, so the environment's file can override anything above.
import_config "#{config_env()}.exs"
import Config
# Nothing specific to development yet.
import Config
# XTS is the ISO 4217 code reserved for testing.
config :bill, currency: "XTS"
import Config
config :bill, currency: "USD"
Code reads it with Application.fetch_env!(:bill, :currency), which raises if
the key is missing, or Application.get_env(:bill, :currency, "EUR"), which
returns a default instead. So Bill.format/1 says EUR under mix run, the
test above expected XTS, and in prod:
$ MIX_ENV=prod mix run -e 'IO.puts(Bill.format(Decimal.new("9.99")))'
$ ls _build
==> decimal
Compiling 4 files (.ex)
Generated decimal app
==> bill
Compiling 1 file (.ex)
Generated bill app
9.99 USD
dev
prod
test
The prod build compiled everything again into _build/prod, and _build
now has one directory per environment that has run.
These files are read when the project is built. Settings that must be read
on the server when the program starts, such as secrets from environment
variables, go in config/runtime.exs, covered in
Runtime Configuration.
Scripts with dependencies
A single script can use Hex packages too. Mix.install/1 at the top fetches
and compiles them on the first run and caches them, so later runs start
quickly:
Mix.install([{:decimal, "~> 3.2"}])
IO.puts(Decimal.add(Decimal.new("0.1"), Decimal.new("0.2")))
Mix.install/1 prints its progress the first time but not later, so the
scripts on this site that use it first call Mix.start() and
Mix.shell(Mix.Shell.Quiet), which silences Mix's own output, so their output
is the same every time.
Pitfall: reading configuration in a module body, as in
@currency Application.fetch_env!(:bill, :currency), runs once at compile time and freezes the value into the compiled code. The compiler warns about it. Read configuration inside functions, asBill.format/1does; Applications and Configuration coversApplication.compile_env/3for the rare value that must be fixed at compile time.
Pitfall:
Mix.install/1is for scripts only. Inside a Mix project it raises "Mix.install/2 cannot be used inside a Mix project"; add the package todepsinmix.exsinstead.
Pitfall: a version requirement without a lock file is a range, not a version. Without
mix.lockin version control, two machines that runmix deps.geta week apart can build different code.