244 lines
8.8 KiB
Elixir
244 lines
8.8 KiB
Elixir
defmodule Mix.Tasks.Compile.ElixirMake do
|
|
@moduledoc """
|
|
Runs `make` in the current project.
|
|
|
|
This task runs `make` in the current project; any output coming from `make` is
|
|
printed in real-time on stdout.
|
|
|
|
## Configuration
|
|
|
|
This compiler can be configured through the return value of the `project/0`
|
|
function in `mix.exs`; for example:
|
|
|
|
def project() do
|
|
[app: :myapp,
|
|
make_executable: "make",
|
|
make_makefile: "Othermakefile",
|
|
compilers: [:elixir_make] ++ Mix.compilers,
|
|
deps: deps()]
|
|
end
|
|
|
|
The following options are available:
|
|
|
|
* `:make_executable` - (binary or `:default`) it's the executable to use as the
|
|
`make` program. If not provided or if `:default`, it defaults to `"nmake"`
|
|
on Windows, `"gmake"` on FreeBSD, OpenBSD and NetBSD, and `"make"` on everything
|
|
else. You can, for example, customize which executable to use on a
|
|
specific OS and use `:default` for every other OS. If the `MAKE`
|
|
environment variable is present, that is used as the value of this option.
|
|
|
|
* `:make_makefile` - (binary or `:default`) it's the Makefile to
|
|
use. Defaults to `"Makefile"` for Unix systems and `"Makefile.win"` for
|
|
Windows systems if not provided or if `:default`.
|
|
|
|
* `:make_targets` - (list of binaries) it's the list of Make targets that
|
|
should be run. Defaults to `[]`, meaning `make` will run the first target.
|
|
|
|
* `:make_clean` - (list of binaries) it's a list of Make targets to be run
|
|
when `mix clean` is run. It's only run if a non-`nil` value for
|
|
`:make_clean` is provided. Defaults to `nil`.
|
|
|
|
* `:make_cwd` - (binary) it's the directory where `make` will be run,
|
|
relative to the root of the project.
|
|
|
|
* `:make_env` - (map of binary to binary) it's a map of extra environment
|
|
variables to be passed to `make`. You can also pass a function in here in
|
|
case `make_env` needs access to things that are not available during project
|
|
setup; the function should return a map of binary to binary. Many default
|
|
environment variables are set, see section below
|
|
|
|
* `:make_error_message` - (binary or `:default`) it's a custom error message
|
|
that can be used to give instructions as of how to fix the error (e.g., it
|
|
can be used to suggest installing `gcc` if you're compiling a C
|
|
dependency).
|
|
|
|
* `:make_args` - (list of binaries) it's a list of extra arguments to be passed.
|
|
|
|
The following options configure precompilation:
|
|
|
|
* `:make_precompiler` - a two-element tuple with the precompiled type
|
|
and module to use. The precompile type is either `:nif` or `:port`
|
|
and then the precompilation module. If the type is a `:nif`, it looks
|
|
for a DDL or a shared object as precompilation target given by
|
|
`:make_precompiler_filename` and the current NIF version is part of
|
|
the precompiled archive. If `:port`, it looks for an executable with
|
|
`:make_precompiler_filename`.
|
|
|
|
* `:make_precompiler_url` - the download URL template. Defaults to none.
|
|
Required when `make_precompiler` is set.
|
|
|
|
* `:make_precompiler_filename` - the filename of the compiled artefact
|
|
without its extension. Defaults to the app name.
|
|
|
|
* `:make_precompiler_downloader` - a module implementing the `ElixirMake.Downloader`
|
|
behaviour. You can use this to customize how the precompiled artefacts
|
|
are downloaded, for example, to add HTTP authentication or to download
|
|
from an SFTP server. The default implementation uses `:httpc`.
|
|
|
|
* `:make_force_build` - if build should be forced even if precompiled artefacts
|
|
are available. Defaults to true if the app has a `-dev` version flag.
|
|
|
|
See [the Precompilation guide](PRECOMPILATION_GUIDE.md) for more information.
|
|
|
|
## Default environment variables
|
|
|
|
There are also several default environment variables set:
|
|
|
|
* `MIX_TARGET`
|
|
* `MIX_ENV`
|
|
* `MIX_BUILD_PATH` - same as `Mix.Project.build_path/0`
|
|
* `MIX_APP_PATH` - same as `Mix.Project.app_path/0`
|
|
* `MIX_COMPILE_PATH` - same as `Mix.Project.compile_path/0`
|
|
* `MIX_CONSOLIDATION_PATH` - same as `Mix.Project.consolidation_path/0`
|
|
* `MIX_DEPS_PATH` - same as `Mix.Project.deps_path/0`
|
|
* `MIX_MANIFEST_PATH` - same as `Mix.Project.manifest_path/0`
|
|
* `ERL_EI_LIBDIR`
|
|
* `ERL_EI_INCLUDE_DIR`
|
|
* `ERTS_INCLUDE_DIR`
|
|
* `ERL_INTERFACE_LIB_DIR`
|
|
* `ERL_INTERFACE_INCLUDE_DIR`
|
|
|
|
These may also be overwritten with the `make_env` option.
|
|
|
|
## Compilation artifacts and working with priv directories
|
|
|
|
Generally speaking, compilation artifacts are written to the `priv`
|
|
directory, as that the only directory, besides `ebin`, which are
|
|
available to Erlang/OTP applications.
|
|
|
|
However, note that Mix projects supports the `:build_embedded`
|
|
configuration, which controls if assets in the `_build` directory
|
|
are symlinked (when `false`, the default) or copied (`true`).
|
|
In order to support both options for `:build_embedded`, it is
|
|
important to follow the given guidelines:
|
|
|
|
* The "priv" directory must not exist in the source code
|
|
* The Makefile should copy any artifact to `$MIX_APP_PATH/priv`
|
|
or, even better, to `$MIX_APP_PATH/priv/$MIX_TARGET`
|
|
* If there are static assets, the Makefile should copy them over
|
|
from a directory at the project root (not named "priv")
|
|
|
|
"""
|
|
|
|
use Mix.Task
|
|
alias ElixirMake.Artefact
|
|
|
|
@recursive true
|
|
|
|
@doc false
|
|
def run(args) do
|
|
if function_exported?(Mix, :ensure_application!, 1) do
|
|
Mix.ensure_application!(:inets)
|
|
Mix.ensure_application!(:ssl)
|
|
Mix.ensure_application!(:crypto)
|
|
end
|
|
|
|
config = Mix.Project.config()
|
|
app = config[:app]
|
|
version = config[:version]
|
|
|
|
force_build =
|
|
pre_release?(version) or Keyword.get(config, :make_force_build, false) or
|
|
Keyword.get(Application.get_env(:elixir_make, :force_build, []), app, false)
|
|
|
|
{precompiler_type, precompiler} = config[:make_precompiler] || {nil, nil}
|
|
|
|
cond do
|
|
precompiler == nil ->
|
|
ElixirMake.Compiler.compile(args)
|
|
|
|
force_build == true ->
|
|
precompiler.build_native(args)
|
|
|
|
true ->
|
|
rootname = config[:make_precompiler_filename] || "#{app}"
|
|
|
|
extname =
|
|
case {precompiler_type, :os.type()} do
|
|
{:nif, {:win32, _}} -> ".dll"
|
|
{:nif, _} -> ".so"
|
|
{:port, {:win32, _}} -> ".exe"
|
|
{:port, _} -> ""
|
|
{_, _} -> raise_unknown_precompiler_type(precompiler_type)
|
|
end
|
|
|
|
app_priv = Path.join(Mix.Project.app_path(config), "priv")
|
|
load_path = Path.join(app_priv, rootname <> extname)
|
|
|
|
with false <- File.exists?(load_path),
|
|
{:error, message} <- download_or_reuse_nif(config, precompiler, app_priv) do
|
|
recover =
|
|
case message do
|
|
{:unavailable_target, current_target, _description} ->
|
|
if function_exported?(precompiler, :unavailable_target, 1) do
|
|
precompiler.unavailable_target(current_target)
|
|
else
|
|
:compile
|
|
end
|
|
|
|
_ ->
|
|
Mix.shell().error("""
|
|
Error happened while installing #{app} from precompiled binary: #{inspect(message)}.
|
|
|
|
Attempting to compile #{app} from source...\
|
|
""")
|
|
|
|
:compile
|
|
end
|
|
|
|
case recover do
|
|
:compile -> precompiler.build_native(args)
|
|
:ignore -> {:ok, []}
|
|
end
|
|
else
|
|
_ -> {:ok, []}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp raise_unknown_precompiler_type(precompiler_type) do
|
|
Mix.raise("Unknown precompiler type: #{inspect(precompiler_type)} (expected :nif or :port)")
|
|
end
|
|
|
|
# This is called by Elixir when `mix clean` runs
|
|
# and `:elixir_make` is in the list of compilers.
|
|
@doc false
|
|
def clean() do
|
|
config = Mix.Project.config()
|
|
{clean_targets, config} = Keyword.pop(config, :make_clean)
|
|
|
|
if clean_targets do
|
|
config
|
|
|> Keyword.put(:make_targets, clean_targets)
|
|
|> ElixirMake.Compiler.make([])
|
|
end
|
|
end
|
|
|
|
defp pre_release?(version) do
|
|
"dev" in Version.parse!(version).pre
|
|
end
|
|
|
|
defp download_or_reuse_nif(config, precompiler, app_priv) do
|
|
nif_version = "#{:erlang.system_info(:nif_version)}"
|
|
|
|
case Artefact.current_target_url(config, precompiler, nif_version) do
|
|
{:ok, target, nif_version_to_use, url} ->
|
|
archived_fullpath = Artefact.archive_path(config, target, nif_version_to_use)
|
|
|
|
unless File.exists?(archived_fullpath) do
|
|
Mix.shell().info("Downloading precompiled NIF to #{archived_fullpath}")
|
|
|
|
with {:ok, archived_data} <- Artefact.download(config, url) do
|
|
File.mkdir_p(Path.dirname(archived_fullpath))
|
|
File.write(archived_fullpath, archived_data)
|
|
end
|
|
end
|
|
|
|
Artefact.verify_and_decompress(archived_fullpath, app_priv)
|
|
|
|
{:error, msg} ->
|
|
{:error, msg}
|
|
end
|
|
end
|
|
end
|