This commit is contained in:
2026-07-14 22:50:46 +04:00
parent 27143319e3
commit bd19e0682b
3116 changed files with 467189 additions and 0 deletions

View File

@@ -0,0 +1,293 @@
defmodule ElixirMake.Artefact do
@moduledoc false
require Logger
alias ElixirMake.Artefact
@checksum_algo :sha256
defstruct [:basename, :checksum, :checksum_algo]
@doc """
Returns user cache directory.
"""
def cache_dir() do
cache_opts = if System.get_env("MIX_XDG"), do: %{os: :linux}, else: %{}
cache_dir =
Path.expand(
System.get_env("ELIXIR_MAKE_CACHE_DIR") ||
:filename.basedir(:user_cache, "elixir_make", cache_opts)
)
File.mkdir_p!(cache_dir)
cache_dir
end
@doc """
Returns the checksum algorithm
"""
def checksum_algo do
@checksum_algo
end
@doc """
Computes the checksum and artefact for the given contents.
"""
def checksum(basename, contents) do
hash = :crypto.hash(checksum_algo(), contents)
checksum = Base.encode16(hash, case: :lower)
%Artefact{basename: basename, checksum: checksum, checksum_algo: checksum_algo()}
end
@doc """
Writes checksum for the target to disk.
"""
def write_checksum_for_target!(%Artefact{
basename: basename,
checksum: checksum,
checksum_algo: checksum_algo
}) do
cache_dir = Artefact.cache_dir()
file = Path.join(cache_dir, "#{basename}.#{Atom.to_string(checksum_algo)}")
File.write!(file, [checksum, " ", basename, "\n"])
end
@doc """
Writes checksums to disk.
"""
def write_checksums!(checksums) do
file = checksum_file()
pairs =
Enum.map(checksums, fn
%Artefact{basename: basename, checksum: checksum, checksum_algo: algo} ->
{basename, "#{algo}:#{checksum}"}
end)
lines =
for {filename, checksum} <- Enum.sort(pairs) do
~s( "#{filename}" => "#{checksum}",\n)
end
File.write!(file, ["%{\n", lines, "}\n"])
end
defp checksum_file() do
Path.join(File.cwd!(), "checksum.exs")
end
## Archive handling
@doc """
Returns the full path to the precompiled archive.
"""
def archive_path(config, target, nif_version) do
Path.join(cache_dir(), archive_filename(config, target, nif_version))
end
defp archive_filename(config, target, nif_version) do
case config[:make_precompiler] do
{:nif, _} ->
"#{config[:app]}-nif-#{nif_version}-#{target}-#{config[:version]}.tar.gz"
{type, _} ->
"#{config[:app]}-#{type}-#{target}-#{config[:version]}.tar.gz"
end
end
@doc """
Compresses the given files and computes its checksum and artefact.
"""
def compress(archive_path, paths) do
:ok = :erl_tar.create(archive_path, paths, [:compressed])
checksum(Path.basename(archive_path), File.read!(archive_path))
end
@doc """
Verifies and decompresses the given `archive_path` at `app_priv`.
"""
def verify_and_decompress(archive_path, app_priv) do
basename = Path.basename(archive_path)
case File.read(archive_path) do
{:ok, contents} ->
verify_and_decompress(basename, archive_path, contents, app_priv)
{:error, reason} ->
{:error,
"precompiled #{inspect(basename)} does not exist or cannot download: #{inspect(reason)}"}
end
end
defp verify_and_decompress(basename, archive_path, contents, app_priv) do
checksum_file()
|> read_map_from_file()
|> case do
%{^basename => algo_with_checksum} ->
[algo, checksum] = String.split(algo_with_checksum, ":")
algo = String.to_existing_atom(algo)
case checksum(basename, contents) do
%Artefact{checksum: ^checksum, checksum_algo: ^algo} ->
case :erl_tar.extract({:binary, contents}, [:compressed, {:cwd, app_priv}]) do
:ok ->
:ok
{:error, term} ->
{:error,
"cannot decompress precompiled #{inspect(archive_path)}: #{inspect(term)}"}
end
_ ->
{:error, "precompiled #{inspect(basename)} does not match its checksum"}
end
checksum when checksum == %{} ->
{:error, "missing checksum.exs file"}
_checksum ->
{:error, "precompiled #{inspect(basename)} does not exist in checksum.exs"}
end
end
defp read_map_from_file(file) do
with {:ok, contents} <- File.read(file),
{%{} = contents, _} <- Code.eval_string(contents) do
contents
else
_ -> %{}
end
end
## Archive/NIF urls
defp nif_version_to_tuple(nif_version) do
[major, minor | _] = String.split(nif_version, ".")
{String.to_integer(major), String.to_integer(minor)}
end
defp fallback_version(opts) do
current_nif_version = "#{:erlang.system_info(:nif_version)}"
{major, minor} = nif_version_to_tuple(current_nif_version)
# Get all matching major versions, earlier than the current version
# and their distance. We want the closest (smallest distance).
candidates =
for version <- opts.versions,
{^major, candidate_minor} <- [nif_version_to_tuple(version)],
candidate_minor <= minor,
do: {minor - candidate_minor, version}
case Enum.sort(candidates) do
[{_, version} | _] -> version
_ -> current_nif_version
end
end
defp get_versions_for_target(versions, current_target) do
case versions do
version_list when is_list(version_list) ->
version_list
version_func when is_function(version_func, 1) ->
version_func.(%{target: current_target})
end
end
@doc """
Returns all available {{target, nif_version}, url} pairs available.
"""
def available_target_urls(config, precompiler) do
targets = precompiler.all_supported_targets(:fetch)
url_template =
config[:make_precompiler_url] ||
Mix.raise("`make_precompiler_url` is not specified in `project`")
current_nif_version = "#{:erlang.system_info(:nif_version)}"
nif_versions =
config[:make_precompiler_nif_versions] ||
[versions: [current_nif_version]]
Enum.reduce(targets, [], fn target, archives ->
versions = get_versions_for_target(nif_versions[:versions], target)
archive_filenames =
Enum.reduce(versions, [], fn nif_version_for_target, acc ->
availability = nif_versions[:availability]
available? =
if is_function(availability, 2) do
IO.warn(
":availability key in elixir_make is deprecated, pass a function as :versions instead"
)
availability.(target, nif_version_for_target)
else
true
end
if available? do
archive_filename = archive_filename(config, target, nif_version_for_target)
[
{{target, nif_version_for_target},
String.replace(url_template, "@{artefact_filename}", archive_filename)}
| acc
]
else
acc
end
end)
archive_filenames ++ archives
end)
end
@doc """
Returns the url for the current target.
"""
def current_target_url(config, precompiler, current_nif_version) do
case precompiler.current_target() do
{:ok, current_target} ->
nif_versions =
config[:make_precompiler_nif_versions] ||
[versions: []]
versions = get_versions_for_target(nif_versions[:versions], current_target)
nif_version_to_use =
if current_nif_version in versions do
current_nif_version
else
fallback_version = nif_versions[:fallback_version] || (&fallback_version/1)
opts = %{target: current_target, versions: versions}
fallback_version.(opts)
end
available_urls = available_target_urls(config, precompiler)
target_at_nif_version = {current_target, nif_version_to_use}
case List.keyfind(available_urls, target_at_nif_version, 0) do
{^target_at_nif_version, download_url} ->
{:ok, current_target, nif_version_to_use, download_url}
nil ->
available_targets = Enum.map(available_urls, fn {target, _url} -> target end)
{:error,
{:unavailable_target, current_target,
"cannot find download url for current target `#{inspect(current_target)}`. Available targets are: #{inspect(available_targets)}"}}
end
{:error, msg} ->
{:error, msg}
end
end
def download(config, url) do
downloader = config[:make_precompiler_downloader] || ElixirMake.Downloader.Httpc
downloader.download(url)
end
end

View File

@@ -0,0 +1,227 @@
defmodule ElixirMake.Compiler do
@moduledoc false
@mac_error_msg """
You need to have gcc and make installed. Try running the
commands "gcc --version" and / or "make --version". If these programs
are not installed, you will be prompted to install them.
"""
@unix_error_msg """
You need to have gcc and make installed. If you are using
Ubuntu or any other Debian-based system, install the packages
"build-essential". Also install "erlang-dev" package if not
included in your Erlang/OTP version. If you're on Fedora, run
"dnf group install 'Development Tools'".
"""
@windows_error_msg ~S"""
One option is to install a recent version of
[Visual C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/)
either manually or using [Chocolatey](https://chocolatey.org/) -
`choco install VisualCppBuildTools`.
After installing Visual C++ Build Tools, look in the "Program Files (x86)"
directory and search for "Microsoft Visual Studio". Note down the full path
of the folder with the highest version number. Open the "run" command and
type in the following command (make sure that the path and version number
are correct):
cmd /K "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" amd64
This should open up a command prompt with the necessary environment variables
set, and from which you will be able to run the "mix compile", "mix deps.compile",
and "mix test" commands.
Another option is to install the Linux compatiblity tools from [MSYS2](https://www.msys2.org/).
After installation start the msys64 bit terminal from the start menu and install the
C/C++ compiler toolchain. E.g.:
pacman -S --noconfirm pacman-mirrors pkg-config
pacman -S --noconfirm --needed base-devel autoconf automake make libtool git \
mingw-w64-x86_64-toolchain mingw-w64-x86_64-openssl mingw-w64-x86_64-libtool
This will give you a compilation suite nearly compatible with Unix' standard tools.
"""
def compile(args) do
config = Mix.Project.config()
Mix.shell().print_app()
priv? = File.dir?("priv")
Mix.Project.ensure_structure()
make(config, args)
# IF there was no priv before and now there is one, we assume
# the user wants to copy it. If priv already existed and was
# written to it, then it won't be copied if build_embedded is
# set to true.
if not priv? and File.dir?("priv") do
Mix.Project.build_structure()
end
{:ok, []}
end
def make(config, task_args) do
exec =
System.get_env("MAKE") ||
os_specific_executable(Keyword.get(config, :make_executable, :default))
makefile = Keyword.get(config, :make_makefile, :default)
targets = Keyword.get(config, :make_targets, [])
env = Keyword.get(config, :make_env, %{})
env = if is_function(env), do: env.(), else: env
env = default_env(config, env)
cwd = Keyword.get(config, :make_cwd, ".") |> Path.expand(File.cwd!())
error_msg = Keyword.get(config, :make_error_message, :default) |> os_specific_error_msg()
custom_args = Keyword.get(config, :make_args, [])
if String.contains?(cwd, " ") do
IO.warn("""
the absolute path to the Makefile for this project contains spaces. \
Make might not work properly if spaces are present in the path. \
The absolute path is: #{inspect(cwd)}
""")
end
base = exec |> Path.basename() |> Path.rootname()
args = args_for_makefile(base, makefile) ++ targets ++ custom_args
case cmd(exec, args, cwd, env, "--verbose" in task_args) do
0 ->
:ok
exit_status ->
raise_build_error(exec, exit_status, error_msg)
end
end
# Runs `exec [args]` in `cwd` and prints the stdout and stderr in real time,
# as soon as `exec` prints them (using `IO.Stream`).
defp cmd(exec, args, cwd, env, verbose?) do
opts = [
# There is no guarantee the command will return valid UTF-8,
# especially on Windows, so don't try to interpret the stream
into: IO.binstream(:stdio, :line),
stderr_to_stdout: true,
cd: cwd,
env: env
]
if verbose? do
print_verbose_info(exec, args)
end
{%IO.Stream{}, status} = System.cmd(find_executable(exec), args, opts)
status
end
defp find_executable(exec) do
System.find_executable(exec) ||
Mix.raise("""
"#{exec}" not found in the path. If you have set the MAKE environment variable, \
please make sure it is correct.
""")
end
defp raise_build_error(exec, exit_status, error_msg) do
Mix.raise(~s{Could not compile with "#{exec}" (exit status: #{exit_status}).\n} <> error_msg)
end
defp os_specific_executable(exec) when is_binary(exec) do
exec
end
defp os_specific_executable(:default) do
case :os.type() do
{:win32, _} ->
cond do
System.find_executable("nmake") -> "nmake"
System.find_executable("make") -> "make"
true -> "nmake"
end
{:unix, type} when type in [:freebsd, :openbsd, :netbsd, :dragonfly] ->
"gmake"
_ ->
"make"
end
end
defp os_specific_error_msg(msg) when is_binary(msg) do
msg
end
defp os_specific_error_msg(:default) do
case :os.type() do
{:unix, :darwin} -> @mac_error_msg
{:unix, _} -> @unix_error_msg
{:win32, _} -> @windows_error_msg
_ -> ""
end
end
# Returns a list of command-line args to pass to make (or nmake/gmake) in
# order to specify the makefile to use.
defp args_for_makefile("nmake", :default), do: ["/F", "Makefile.win"]
defp args_for_makefile("nmake", makefile), do: ["/F", makefile]
defp args_for_makefile(_, :default), do: []
defp args_for_makefile(_, makefile), do: ["-f", makefile]
defp print_verbose_info(exec, args) do
args =
Enum.map_join(args, " ", fn arg ->
if String.contains?(arg, " "), do: inspect(arg), else: arg
end)
Mix.shell().info("Compiling with make: #{exec} #{args}")
end
# Returns a map of default environment variables
# Defaults may be overwritten.
defp default_env(config, default_env) do
root_dir = :code.root_dir()
erl_interface_dir = Path.join(root_dir, "usr")
erts_dir = Path.join(root_dir, "erts-#{:erlang.system_info(:version)}")
erts_include_dir = Path.join(erts_dir, "include")
erl_ei_lib_dir = Path.join(erl_interface_dir, "lib")
erl_ei_include_dir = Path.join(erl_interface_dir, "include")
Map.merge(
%{
# Don't use Mix.target/0 here for backwards compatibility
"MIX_TARGET" => env("MIX_TARGET", "host"),
"MIX_ENV" => to_string(Mix.env()),
"MIX_BUILD_PATH" => Mix.Project.build_path(config),
"MIX_APP_PATH" => Mix.Project.app_path(config),
"MIX_COMPILE_PATH" => Mix.Project.compile_path(config),
"MIX_CONSOLIDATION_PATH" => Mix.Project.consolidation_path(config),
"MIX_DEPS_PATH" => Mix.Project.deps_path(config),
"MIX_MANIFEST_PATH" => Mix.Project.manifest_path(config),
# Rebar naming
"ERL_EI_LIBDIR" => env("ERL_EI_LIBDIR", erl_ei_lib_dir),
"ERL_EI_INCLUDE_DIR" => env("ERL_EI_INCLUDE_DIR", erl_ei_include_dir),
# erlang.mk naming
"ERTS_INCLUDE_DIR" => env("ERTS_INCLUDE_DIR", erts_include_dir),
"ERL_INTERFACE_LIB_DIR" => env("ERL_INTERFACE_LIB_DIR", erl_ei_lib_dir),
"ERL_INTERFACE_INCLUDE_DIR" => env("ERL_INTERFACE_INCLUDE_DIR", erl_ei_include_dir),
# Disable default erlang values
"BINDIR" => nil,
"ROOTDIR" => nil,
"PROGNAME" => nil,
"EMU" => nil
},
default_env
)
end
defp env(var, default) do
System.get_env(var) || default
end
end

View File

@@ -0,0 +1,10 @@
defmodule ElixirMake.Downloader do
@moduledoc """
The behaviour for downloader modules.
"""
@doc """
This callback should download the artefact from the given URL.
"""
@callback download(url :: String.t()) :: {:ok, iolist() | binary()} | {:error, String.t()}
end

View File

@@ -0,0 +1,99 @@
defmodule ElixirMake.Downloader.Httpc do
@moduledoc false
@behaviour ElixirMake.Downloader
@impl ElixirMake.Downloader
def download(url) do
url_charlist = String.to_charlist(url)
# TODO: Remove me when we require Elixir v1.15
{:ok, _} = Application.ensure_all_started(:inets)
{:ok, _} = Application.ensure_all_started(:ssl)
{:ok, _} = Application.ensure_all_started(:public_key)
if proxy = System.get_env("HTTP_PROXY") || System.get_env("http_proxy") do
Mix.shell().info("Using HTTP_PROXY: #{proxy}")
%{host: host, port: port} = URI.parse(proxy)
:httpc.set_options([{:proxy, {{String.to_charlist(host), port}, []}}])
end
if proxy = System.get_env("HTTPS_PROXY") || System.get_env("https_proxy") do
Mix.shell().info("Using HTTPS_PROXY: #{proxy}")
%{host: host, port: port} = URI.parse(proxy)
:httpc.set_options([{:https_proxy, {{String.to_charlist(host), port}, []}}])
end
# https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/inets
# TODO: This may no longer be necessary from Erlang/OTP 25.0 or later.
https_options = [
ssl:
[
verify: :verify_peer,
customize_hostname_check: [
match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
]
] ++ cacerts_options()
]
options = [body_format: :binary]
case :httpc.request(:get, {url_charlist, []}, https_options, options) do
{:ok, {{_, 200, _}, _headers, body}} ->
{:ok, body}
other ->
{:error, "couldn't fetch NIF from #{url}: #{inspect(other)}"}
end
end
defp cacerts_options do
cond do
path = System.get_env("HEX_CACERTS_PATH") ->
[cacertfile: path]
path = System.get_env("ELIXIR_MAKE_CACERT") ->
IO.warn("Setting ELIXIR_MAKE_CACERT is deprecated, please set HEX_CACERTS_PATH instead")
[cacertfile: path]
certs = otp_cacerts() ->
[cacerts: certs]
true ->
warn_no_cacerts()
[]
end
end
defp otp_cacerts do
if System.otp_release() >= "25" do
# cacerts_get/0 raises if no certs found
try do
:public_key.cacerts_get()
rescue
_ -> nil
end
end
end
defp warn_no_cacerts do
Mix.shell().error("""
No certificate trust store was found.
A certificate trust store is required in
order to download locales for your configuration.
Since elixir_make could not detect a system
installed certificate trust store one of the
following actions may be taken:
1. Specify the location of a certificate trust store
by configuring it in environment variable:
export HEX_CACERTS_PATH="/path/to/cacerts.pem"
2. Use OTP 25+ on an OS that has built-in certificate
trust store.
""")
end
end

View File

@@ -0,0 +1,110 @@
defmodule ElixirMake.Precompiler do
@moduledoc """
The behaviour for precompiler modules.
"""
require Logger
@typedoc """
Target triplet.
"""
@type target :: String.t()
@doc """
This callback should return a list of triplets ("arch-os-abi") for all supported targets
of the given operation.
For the `:compile` operation, `all_supported_targets` should return a list of targets that
the current host is capable of (cross-)compiling to.
For the `:fetch` operation, `all_supported_targets` should return the full list of targets.
For example, GitHub Actions provides Linux, macOS and Windows CI hosts, when `operation` is
`:compile`, the precompiler might return `["x86_64-linux-gnu"]` if it is running in the Linux
CI environment, while returning `["x86_64-apple-darwin", "aarch64-apple-darwin"]` on macOS,
or `["amd64-windows", "x86-windows"]` on Windows platform.
When `operation` is `:fetch`, the precompiler should return the full list. The full list for
the above example should be:
[
"x86_64-linux-gnu",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"amd64-windows",
"x86-windows"
]
This allows the precompiler to do the compilation work in multilple hosts and gather all the
artefacts later with `mix elixir_make.checksum --all`.
"""
@callback all_supported_targets(operation :: :compile | :fetch) :: [target]
@doc """
This callback should return the target triplet for current node.
"""
@callback current_target() :: {:ok, target} | {:error, String.t()}
@doc """
This callback will be invoked when the user executes the `mix compile`
(or `mix compile.elixir_make`) command.
The precompiler should then compile the NIF library "natively". Note that
it is possible for the precompiler module to pick up other environment variables
like `TARGET_ARCH=aarch64` and adjust compile arguments correspondingly.
"""
@callback build_native(OptionParser.argv()) ::
{Mix.Task.Compiler.status(), [Mix.Task.Compiler.Diagnostic.t()]}
@doc """
This callback should precompile the library to the given target(s).
Returns `:ok` if the requested target has successfully compiled.
"""
@callback precompile(OptionParser.argv(), target) :: :ok | {:error, String.t()}
@doc """
Optional post actions to run after each precompilation target is archived.
It will be called when a target is precompiled and archived successfully.
For example, actions can be deleting all target-specific files.
"""
@callback post_precompile_target(target) :: :ok
@doc """
Optional post actions to run after all precompilation tasks are done.
It will only be called at the end of the `mix elixir_make.precompile` command.
For example, actions can be archiving all precompiled artefacts and uploading
the archive file to an object storage server.
"""
@callback post_precompile() :: :ok
@doc """
Optional recover actions when the current target is unavailable.
There are two reasons that the current target might be unavailable:
when the library only has precompiled binaries for some platforms,
and it either
- needs to be compiled on other platforms.
The callback should return `:compile` for this case.
- is intended to function as `noop` on other platforms.
The callback should return `:ignore` for this case.
Defaults to `:compile` if this callback is not implemented.
"""
@callback unavailable_target(String.t()) :: :compile | :ignore
@optional_callbacks post_precompile: 0, unavailable_target: 1, post_precompile_target: 1
@doc """
Invoke the regular Mix toolchain compilation.
"""
def mix_compile(args) do
ElixirMake.Compiler.compile(args)
end
end

View File

@@ -0,0 +1,243 @@
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

View File

@@ -0,0 +1,172 @@
defmodule Mix.Tasks.ElixirMake.Checksum do
@shortdoc "Fetch precompiled NIFs and build the checksums"
@moduledoc """
A task responsible for downloading the precompiled NIFs for a given module.
This task must only be used by package creators who want to ship the
precompiled NIFs. The goal is to download the precompiled packages and
generate a checksum to check-in alongside the project in the the Hex repository.
This is done by passing the `--all` flag.
You can also use the `--only-local` flag to download only the precompiled
package for use during development.
You can use the `--ignore-unavailable` flag to ignore any NIFs that are not available.
This is useful when you are developing a new NIF that does not support all platforms.
This task also accept the `--print` flag to print the checksums.
"""
use Mix.Task
alias ElixirMake.Artefact
@recursive true
@switches [
all: :boolean,
only_local: :boolean,
print: :boolean,
ignore_unavailable: :boolean
]
@impl true
def run(flags) when is_list(flags) 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()
{_, precompiler} =
config[:make_precompiler] ||
Mix.raise(
":make_precompiler project configuration is required when using elixir_make.checksum"
)
{options, _args} = OptionParser.parse!(flags, strict: @switches)
urls =
cond do
Keyword.get(options, :all) ->
Artefact.available_target_urls(config, precompiler)
Keyword.get(options, :only_local) ->
current_nif_version = "#{:erlang.system_info(:nif_version)}"
case Artefact.current_target_url(config, precompiler, current_nif_version) do
{:ok, target, nif_version_to_use, url} ->
[{{target, nif_version_to_use}, url}]
{:error, {:unavailable_target, current_target, error}} ->
recover =
if function_exported?(precompiler, :unavailable_target, 1) do
precompiler.unavailable_target(current_target)
else
:compile
end
case recover do
:compile ->
Mix.raise(error)
:ignore ->
[]
end
{:error, error} ->
Mix.raise(error)
end
true ->
Mix.raise("you need to specify either \"--all\" or \"--only-local\" flags")
end
artefacts = download_and_checksum_all(config, urls, options)
if Keyword.get(options, :print, false) do
artefacts
|> Enum.map(fn %Artefact{basename: basename, checksum: checksum} -> {basename, checksum} end)
|> Enum.sort()
|> Enum.map_join("\n", fn {file, checksum} -> "#{checksum} #{file}" end)
|> IO.puts()
end
Artefact.write_checksums!(artefacts)
end
defp download_and_checksum_all(config, urls, options) do
ignore_unavailable? = Keyword.get(options, :ignore_unavailable, false)
tasks =
Task.async_stream(
urls,
fn {{_target, _nif_version}, url} ->
checksum_algo = Artefact.checksum_algo()
checksum_file_url = "#{url}.#{Atom.to_string(checksum_algo)}"
artifact_checksum = Artefact.download(config, checksum_file_url)
with {:ok, body} <- artifact_checksum,
[checksum, basename] <- String.split(body, " ", trim: true) do
{:checksum, url,
%Artefact{
basename: String.trim(basename),
checksum: checksum,
checksum_algo: checksum_algo
}}
else
_ -> {:download, url, Artefact.download(config, url)}
end
end,
timeout: :infinity,
ordered: false
)
cache_dir = Artefact.cache_dir()
Enum.flat_map(tasks, fn
{:ok, {:checksum, _url, artefact}} ->
Mix.shell().info(
"NIF checksum file with checksum #{artefact.checksum} (#{artefact.checksum_algo})"
)
[artefact]
{:ok, {:download, url, download}} ->
case download do
{:ok, body} ->
basename = basename_from_url(url)
path = Path.join(cache_dir, basename)
File.write!(path, body)
artefact = Artefact.checksum(basename, body)
Mix.shell().info(
"NIF cached at #{path} with checksum #{artefact.checksum} (#{artefact.checksum_algo})"
)
[artefact]
result ->
if ignore_unavailable? do
msg = "Skipped unavailable NIF artifact. Reason: #{inspect(result)}"
Mix.shell().info(msg)
else
msg = "Could not finish the download of NIF artifacts. Reason: #{inspect(result)}"
Mix.shell().error(msg)
end
[]
end
end)
end
defp basename_from_url(url) do
uri = URI.parse(url)
uri.path
|> String.split("/")
|> List.last()
end
end

View File

@@ -0,0 +1,103 @@
defmodule Mix.Tasks.ElixirMake.Precompile do
@shortdoc "Precompiles the given project for all targets"
@moduledoc """
Precompiles the given project for all targets.
This task must only be used by package creators who want to ship the
precompiled NIFs. This task is often used on CI to precompile
for different targets.
This is only supported if `:make_precompiler` is specified
in your project configuration.
"""
alias ElixirMake.Artefact
require Logger
use Mix.Task
@recursive true
@impl true
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()
paths = config[:make_precompiler_priv_paths] || ["."]
{_, precompiler} =
config[:make_precompiler] ||
Mix.raise(
":make_precompiler project configuration is required when using elixir_make.precompile"
)
targets = precompiler.all_supported_targets(:compile)
try do
precompiled_artefacts =
Enum.map(targets, fn target ->
case precompiler.precompile(args, target) do
:ok ->
precompiled_artefacts = create_precompiled_archive(config, target, paths)
if function_exported?(precompiler, :post_precompile_target, 1) do
precompiler.post_precompile_target(target)
end
Artefact.write_checksum_for_target!(precompiled_artefacts)
precompiled_artefacts
{:error, msg} ->
Mix.raise(msg)
end
end)
Artefact.write_checksums!(precompiled_artefacts)
if function_exported?(precompiler, :post_precompile, 0) do
precompiler.post_precompile()
else
:ok
end
after
app_priv = Path.join(Mix.Project.app_path(config), "priv")
for include <- paths,
file <- Path.wildcard(Path.join(app_priv, include)) do
File.rm_rf(file)
end
end
end
defp create_precompiled_archive(config, target, paths) do
archive_path = Artefact.archive_path(config, target, :erlang.system_info(:nif_version))
Mix.shell().info("Creating precompiled archive: #{archive_path}")
Mix.shell().info("Paths to archive from priv directory: #{inspect(paths)}")
app_priv = Path.join(Mix.Project.app_path(config), "priv")
File.mkdir_p!(app_priv)
File.mkdir_p!(Path.dirname(archive_path))
artefact =
File.cd!(app_priv, fn ->
filepaths =
for path <- paths,
entry <- Path.wildcard(path),
do: String.to_charlist(entry)
Artefact.compress(archive_path, filepaths)
end)
Mix.shell().info(
"NIF cached at #{archive_path} with checksum #{artefact.checksum} (#{artefact.checksum_algo})"
)
artefact
end
end