defmodule Mix.Tasks.Compile.LinxProcess do # Mix compiler for the `linx_process` Port binary. # # Compiles `c_src/linx_process.c` into a standalone executable in the # application's `priv/` directory, where `Linx.Process` spawns it via # `Port.open`. Runs as part of `mix compile` (and therefore `iex -S mix`, # `mix test`, releases), so there is no separate Makefile step. # # Set `LINX_DEBUG=1` to build with `-g -O0`. Override the compiler with # the `CC` environment variable. @moduledoc false use Mix.Task.Compiler @source "c_src/linx_process.c" @artifact "linx_process" @impl true def run(_args) do source = Path.absname(@source) output = output_path() cond do not File.exists?(source) -> Mix.raise("linx_process: missing C source #{@source}") stale?(source, output) -> File.mkdir_p!(Path.dirname(output)) compile(source, output) true -> :noop end end @impl true def clean do File.rm(output_path()) :ok end # The executable lives in the build's priv dir, where :code.priv_dir/1 # finds it at runtime; nothing is written into the source tree. defp output_path do Path.join([Mix.Project.app_path(), "priv", @artifact]) end defp stale?(source, output) do case {File.stat(source), File.stat(output)} do {{:ok, src}, {:ok, out}} -> src.mtime > out.mtime _ -> true end end defp compile(source, output) do cc = System.get_env("CC", "cc") Mix.Linx.Preflight.check!(cc) debug = if System.get_env("LINX_DEBUG") in ~w(1 true yes), do: ~w(-g -O0), else: ~w(-O2) args = ~w(-std=c11 -Wall -Wextra -Wpedantic -D_GNU_SOURCE) ++ debug ++ ["-I", ei_include_dir(), "-L", ei_lib_dir()] ++ ["-o", output, source] ++ ~w(-lei -lpthread) Mix.shell().info("compiling #{@source} -> priv/#{@artifact}") case System.cmd(cc, args, stderr_to_stdout: true) do {_output, 0} -> :ok {output, status} -> Mix.raise("linx_process: #{cc} failed (exit #{status})\n#{output}") end end # erl_interface ships with OTP under `erl_interface-/`; libei.a # is the static archive we link in. :code.lib_dir/1 is unreliable here # (the app may not be loaded during `mix compile`), so we resolve the # directory by globbing under the OTP root -- same strategy # `compile.netlink_nif` uses for the erts include dir. # # Cross-compile setups (notably Nerves) point `ERL_EI_INCLUDE_DIR` and # `ERL_EI_LIBDIR` at the *target* Erlang's erl_interface; honour those # before falling back to the host OTP layout so we don't link the host # libei.a into a cross-built binary. defp ei_dir do case Path.wildcard(Path.join(:code.root_dir(), "lib/erl_interface-*")) do [] -> Mix.raise("linx_process: erl_interface not found under #{:code.root_dir()}") dirs -> dirs |> Enum.sort() |> List.last() end end defp ei_include_dir do case System.get_env("ERL_EI_INCLUDE_DIR") do nil -> Path.join(ei_dir(), "include") "" -> Path.join(ei_dir(), "include") dir -> dir end end defp ei_lib_dir do case System.get_env("ERL_EI_LIBDIR") do nil -> Path.join(ei_dir(), "lib") "" -> Path.join(ei_dir(), "lib") dir -> dir end end end