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,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