Current section
Files
Jump to
Current section
Files
lib/mix/tasks/fennec.precompile.ex
defmodule Mix.Tasks.Fennec.Precompile do
@moduledoc """
Download and use precompiled NIFs safely with checksums.
Fennec Precompile is a tool for library maintainers that use `:elixir_make`
and wish to ship precompiled binaries. This tool aims to be a drop-in
replacement for `:elixir_make`.
It helps by removing the need to have the C/C++ compiler and other dependencies
installed in the user's machine.
Check the [Precompilation Guide](PRECOMPILATION_GUIDE.md) for details.
"""
use Mix.Task
require Logger
@user_config Application.compile_env(:fennec_precompile, :config, [])
@return if Version.match?(System.version(), "~> 1.9"), do: {:ok, []}, else: :ok
@impl true
def run(args) do
build_with_targets(args, compile_targets(), true)
end
def build_with_targets(args, targets, post_clean) do
saved_cwd = File.cwd!()
cache_dir = System.get_env("FENNEC_CACHE_DIR", nil)
if cache_dir do
System.put_env("FENNEC_CACHE_DIR", cache_dir)
end
cache_dir = FennecPrecompile.cache_dir("")
app = get_app_name()
do_fennec_precompile(app, args, targets, saved_cwd, cache_dir)
if post_clean do
make_priv_dir(app, :clean)
else
with {:ok, target} <- FennecPrecompile.target(targets) do
version = get_app_version()
nif_version = "#{:erlang.system_info(:nif_version)}"
tar_filename = "#{app}-nif-#{nif_version}-#{target}-#{version}.tar.gz"
cached_tar_gz = Path.join([cache_dir, tar_filename])
FennecPrecompile.restore_nif_file(cached_tar_gz, app)
end
end
Mix.Project.build_structure()
@return
end
def build_native_using_zig(args) do
with {:ok, target} <- get_native_target() do
build_with_targets(args, [target], false)
end
end
def build_native(args) do
if always_use_zig?() do
build_native_using_zig(args)
else
Mix.Tasks.Compile.ElixirMake.run(args)
end
end
defp get_native_target() do
with {:ok, targets} <- FennecPrecompile.target(FennecPrecompile.Config.default_targets()) do
{:ok, targets}
else
_ ->
custom_native_target = System.get_env("FENNEC_PRECOMPILE_NATIVE_TARGET")
if custom_native_target == nil do
raise RuntimeError, "Cannot identify triplets for native target"
else
{:ok, custom_native_target}
end
end
end
defp always_use_zig?() do
always_use_zig?(System.get_env("FENNEC_PRECOMPILE_ALWAYS_USE_ZIG", "NO"))
end
defp always_use_zig?("true"), do: true
defp always_use_zig?("TRUE"), do: true
defp always_use_zig?("YES"), do: true
defp always_use_zig?("yes"), do: true
defp always_use_zig?("y"), do: true
defp always_use_zig?("on"), do: true
defp always_use_zig?("ON"), do: true
defp always_use_zig?(_), do: false
defp compile_targets() do
targets = System.get_env("FENNEC_PRECOMPILE_TARGETS")
if targets do
String.split(targets, ",", trim: true)
else
app = get_app_name()
user_targets = Keyword.get(Keyword.get(@user_config, app, []), :targets)
if user_targets != nil do
user_targets
else
FennecPrecompile.Config.default_targets()
end
end
end
defp do_fennec_precompile(app, args, targets, saved_cwd, cache_dir) do
saved_cc = System.get_env("CC") || ""
saved_cxx = System.get_env("CXX") || ""
saved_cpp = System.get_env("CPP") || ""
checksums = fennec_precompile(app, args, targets, cache_dir)
FennecPrecompile.write_checksum!(app, checksums)
File.cd!(saved_cwd)
System.put_env("CC", saved_cc)
System.put_env("CXX", saved_cxx)
System.put_env("CPP", saved_cpp)
end
defp fennec_precompile(app, args, targets, cache_dir) do
Enum.reduce(targets, [], fn target, checksums ->
Logger.debug("Current compiling target: #{target}")
make_priv_dir(app, :clean)
{cc, cxx} =
case {:os.type(), target} do
{{:unix, :darwin}, "x86_64-macos" <> _} ->
{"gcc -arch x86_64", "g++ -arch x86_64"}
{{:unix, :darwin}, "aarch64-macos" <> _} ->
{"gcc -arch arm64", "g++ -arch arm64"}
_ ->
{"zig cc -target #{target}", "zig c++ -target #{target}"}
end
System.put_env("CC", cc)
System.put_env("CXX", cxx)
System.put_env("CPP", cxx)
Mix.Tasks.Compile.ElixirMake.run(args)
{archive_full_path, archive_tar_gz} = create_precompiled_archive(target, cache_dir)
{:ok, algo, checksum} = FennecPrecompile.compute_checksum(archive_full_path, :sha256)
[%{path: archive_tar_gz, checksum_algo: algo, checksum: checksum} | checksums]
end)
end
defp create_precompiled_archive(target, cache_dir) do
saved_cwd = File.cwd!()
app = get_app_name()
version = get_app_version()
app_priv = app_priv(app)
File.cd!(app_priv)
nif_version = FennecPrecompile.current_nif_version()
archive_filename = "#{app}-nif-#{nif_version}-#{target}-#{version}"
archive_tar_gz = "#{archive_filename}.tar.gz"
archive_full_path = Path.expand(Path.join([cache_dir, archive_tar_gz]))
Logger.debug("Creating precompiled archive: #{archive_full_path}")
System.cmd("tar", ["-czf", archive_full_path, "."])
File.cd!(saved_cwd)
{archive_full_path, archive_tar_gz}
end
defp get_app_name() do
System.get_env("FENNEC_PRECOMPILE_OTP_APP", "#{Mix.Project.config()[:app]}")
|> String.to_atom()
end
defp get_app_version() do
System.get_env("FENNEC_PRECOMPILE_VERSION", "#{Mix.Project.config()[:version]}")
end
defp app_priv(app) when is_atom(app) do
build_path = Mix.Project.build_path()
Path.join([build_path, "lib", "#{app}", "priv"])
end
defp make_priv_dir(app, :clean) when is_atom(app) do
app_priv = app_priv(app)
File.rm_rf!(app_priv)
make_priv_dir(app)
end
defp make_priv_dir(app) when is_atom(app) do
File.mkdir_p!(app_priv(app))
end
end