featurebranch.com

GitHub RSS

Using mix to compile your Erlang projects

My talk at ElixirConf 2014 touched on how the Sauspiel game server - while written in Erlang - uses mix for dependency management and for compliation. I was asked in #elixir-lang to elaborate on that.

So without further ado here’s how you too can get rid of rebar for compiling your Erlang apps and their dependencies (though we still use it for building the release):

  1. We’re gonna be building an umbrella project (which in rebar-speak just means you’ve got sub_dirs), so we need to put a mix.exs both in the project’s root and in each apps/* sub directory.

  2. To DRY up the configuration we’ve placed those parts of the app configuration that are the same in every app into the root mix.exs and read it from there.

  3. We’ll need a custom mix task to copy the .app.src file to the ebin directory.

  4. When building for testing (which we’ve -IFDEF-ed away) we want to turn the TEST define on.

Here’s the root mix.exs:

defmodule Sauspiel.Mixfile do
  use Mix.Project

  def project do
    [elixir: "0.14.3",
     apps_path: "apps",
     compile_path: ".",
     default_task: "compile",
     deps: deps]
  end

  def subproject_defaults do
    Dict.merge [
      elixir: project[:elixir],
      compilers: [:copyapp, :erlang],
      default_task: "compile",
      erlc_options: erlc_options,
      deps: deps,
      deps_path: "../../deps",
      lockfile: "../../mix.lock",
      compile_path: ".",
    ], subproject_defaults(Mix.env)
  end

  defp subproject_defaults(:test), do: [
    # Compile tests and -IFDEF'ed parts of source files
    erlc_paths: ["src", "test"],
    erlc_options: [{:d, :TEST}] ++ erlc_options
  ]
  defp subproject_defaults(_), do: []

  defp erlc_options do
    # Using Mix.Project.compile_path here raises an exception,
    # use Mix.Project.config[:compile_path] instead.
    root_path = Path.expand("../..", Mix.Project.config[:compile_path])
    includes = Path.wildcard(Path.join(root_path, "apps/*/include"))
    [:debug_info] ++ Enum.map(includes, fn(path) -> {:i, path} end)
  end

  defp, deps do: [] # you know what goes here
end

defmodule Mix.Tasks.Compile.Copyapp do
  use Mix.Task

  def run(_) do
    project      = Mix.Project.config
    source_paths = project[:erlc_paths]
    compile_path = Mix.Project.compile_path
    files        = Mix.Utils.extract_files(source_paths, "*.app.src")

    File.mkdir_p!(compile_path)
    Enum.each(files, fn(app_src_file) ->
      app_file = Path.join(compile_path, Path.basename(app_src_file, ".src"))
      File.copy!(app_src_file, app_file)
    end)
  end
end

and one of the mix.exs in the apps/* sub directories:

Code.require_file "mix.exs", "../.."

defmodule Sauspiel.Cardgame.Mixfile do
  use Mix.Project

  def project do
    Dict.merge(Sauspiel.Mixfile.subproject_defaults, [
      app: :cardgame,
      deps: [ {:storage, in_umbrella: true} ],
    ])
  end
end

Bonus: Turning mix into an escript

This is actually really easy. Right now you have to check out a pull request, but I hope that it gets merged soon.

After that just cd lib/mix and mix escript.build

Voila!

Thoughts? Tweet me @MSch, send an email or leave a comment below.