update
This commit is contained in:
576
phoenix/deps/cc_precompiler/lib/cc_precompiler.ex
Normal file
576
phoenix/deps/cc_precompiler/lib/cc_precompiler.ex
Normal file
@@ -0,0 +1,576 @@
|
||||
defmodule CCPrecompiler do
|
||||
@moduledoc """
|
||||
Precompile with existing crosscompiler in the system.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
@behaviour ElixirMake.Precompiler
|
||||
|
||||
# The default configuration for this precompiler module on linux systems.
|
||||
# It will detect for the following targets
|
||||
# - x86_64-linux-gnu
|
||||
# - i686-linux-gnu
|
||||
# - aarch64-linux-gnu
|
||||
# - armv7l-linux-gnueabihf
|
||||
# - riscv64-linux-gnu
|
||||
# - powerpc64le-linux-gnu
|
||||
# - s390x-linux-gnu
|
||||
# by trying to find the corresponding executable, i.e.,
|
||||
# - x86_64-linux-gnu-gcc
|
||||
# - i686-linux-gnu-gcc
|
||||
# - aarch64-linux-gnu-gcc
|
||||
# - arm-linux-gnueabihf-gcc
|
||||
# - riscv64-linux-gnu-gcc
|
||||
# - powerpc64le-linux-gnu-gcc
|
||||
# - s390x-linux-gnu-gcc
|
||||
# (this module will only try to find the CC executable, a step further
|
||||
# will be trying to compile a simple C/C++ program using them)
|
||||
@default_compilers %{
|
||||
{:unix, :linux} => %{
|
||||
"x86_64-linux-gnu" => "x86_64-linux-gnu-",
|
||||
"i686-linux-gnu" => "i686-linux-gnu-",
|
||||
"aarch64-linux-gnu" => "aarch64-linux-gnu-",
|
||||
"armv7l-linux-gnueabihf" => "arm-linux-gnueabihf-",
|
||||
"riscv64-linux-gnu" => "riscv64-linux-gnu-",
|
||||
"powerpc64le-linux-gnu" => "powerpc64le-linux-gnu-",
|
||||
"s390x-linux-gnu" => "s390x-linux-gnu-"
|
||||
},
|
||||
{:unix, :darwin} => %{
|
||||
"x86_64-apple-darwin" => {
|
||||
"gcc",
|
||||
"g++",
|
||||
"<%= cc %> -arch x86_64",
|
||||
"<%= cxx %> -arch x86_64"
|
||||
},
|
||||
"aarch64-apple-darwin" => {
|
||||
"gcc",
|
||||
"g++",
|
||||
"<%= cc %> -arch arm64",
|
||||
"<%= cxx %> -arch arm64"
|
||||
}
|
||||
},
|
||||
{:win32, :nt} => %{
|
||||
"x86_64-windows-msvc" => {"cl", "cl"}
|
||||
}
|
||||
}
|
||||
defp default_compilers, do: @default_compilers
|
||||
defp user_config, do: Mix.Project.config()[:cc_precompiler] || default_compilers()
|
||||
defp compilers, do: Access.get(user_config(), :compilers, default_compilers())
|
||||
|
||||
defp compilers_current_os,
|
||||
do:
|
||||
{Access.get(compilers(), :os.type(), %{}), Access.get(default_compilers(), :os.type(), %{})}
|
||||
|
||||
defp compilers_current_os_with_override do
|
||||
{compiler_map1, compiler_map2} = compilers_current_os()
|
||||
|
||||
if Map.has_key?(compiler_map1, :include_default_ones) do
|
||||
include_default_ones = Map.get(compiler_map1, :include_default_ones, false)
|
||||
compiler_map1 = Map.delete(compiler_map1, :include_default_ones)
|
||||
|
||||
if include_default_ones == true do
|
||||
Map.merge(compiler_map1, compiler_map2, fn _, _, user_override -> user_override end)
|
||||
else
|
||||
compiler_map1
|
||||
end
|
||||
else
|
||||
compiler_map1
|
||||
end
|
||||
end
|
||||
|
||||
defp only_listed_targets, do: Access.get(user_config(), :only_listed_targets, false)
|
||||
defp exclude_current_target, do: Access.get(user_config(), :exclude_current_target, false)
|
||||
defp allow_missing_compiler, do: Access.get(user_config(), :allow_missing_compiler, false)
|
||||
|
||||
@impl ElixirMake.Precompiler
|
||||
def current_target do
|
||||
current_target_from_env = current_target_from_env()
|
||||
|
||||
if current_target_from_env do
|
||||
# overwrite current target triplet from environment variables
|
||||
{:ok, current_target_from_env}
|
||||
else
|
||||
current_target(:os.type())
|
||||
end
|
||||
end
|
||||
|
||||
defp current_target_from_env do
|
||||
arch = System.get_env("TARGET_ARCH")
|
||||
os = System.get_env("TARGET_OS")
|
||||
abi = System.get_env("TARGET_ABI")
|
||||
|
||||
if !Enum.all?([arch, os, abi], &Kernel.is_nil/1) do
|
||||
"#{arch}-#{os}-#{abi}"
|
||||
end
|
||||
end
|
||||
|
||||
def current_target({:win32, _}) do
|
||||
processor_architecture =
|
||||
String.downcase(String.trim(System.get_env("PROCESSOR_ARCHITECTURE")))
|
||||
|
||||
# https://docs.microsoft.com/en-gb/windows/win32/winprog64/wow64-implementation-details?redirectedfrom=MSDN
|
||||
partial_triplet =
|
||||
case processor_architecture do
|
||||
"amd64" ->
|
||||
"x86_64-windows-"
|
||||
|
||||
"ia64" ->
|
||||
"ia64-windows-"
|
||||
|
||||
"arm64" ->
|
||||
"aarch64-windows-"
|
||||
|
||||
"x86" ->
|
||||
"x86-windows-"
|
||||
end
|
||||
|
||||
{compiler, _} = :erlang.system_info(:c_compiler_used)
|
||||
|
||||
case compiler do
|
||||
:msc ->
|
||||
{:ok, partial_triplet <> "msvc"}
|
||||
|
||||
:gnuc ->
|
||||
{:ok, partial_triplet <> "gnu"}
|
||||
|
||||
other ->
|
||||
{:ok, partial_triplet <> Atom.to_string(other)}
|
||||
end
|
||||
end
|
||||
|
||||
def current_target({:unix, _}) do
|
||||
# get current target triplet from `:erlang.system_info/1`
|
||||
system_architecture = to_string(:erlang.system_info(:system_architecture))
|
||||
current = String.split(system_architecture, "-", trim: true)
|
||||
|
||||
case length(current) do
|
||||
4 ->
|
||||
{:ok, "#{Enum.at(current, 0)}-#{Enum.at(current, 2)}-#{Enum.at(current, 3)}"}
|
||||
|
||||
3 ->
|
||||
case :os.type() do
|
||||
{:unix, :darwin} ->
|
||||
# could be something like aarch64-apple-darwin21.0.0
|
||||
# but we don't really need the last 21.0.0 part
|
||||
if String.match?(Enum.at(current, 2), ~r/^darwin.*/) do
|
||||
{:ok, "#{Enum.at(current, 0)}-#{Enum.at(current, 1)}-darwin"}
|
||||
else
|
||||
{:ok, system_architecture}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:ok, system_architecture}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, "cannot decide current target"}
|
||||
end
|
||||
end
|
||||
|
||||
defp only_local do
|
||||
System.get_env("CC_PRECOMPILER_PRECOMPILE_ONLY_LOCAL") == "true"
|
||||
end
|
||||
|
||||
@impl ElixirMake.Precompiler
|
||||
def all_supported_targets(:compile) do
|
||||
# this callback is expected to return a list of string for
|
||||
# all supported targets by this precompiler. in this
|
||||
# implementation, we will try to find a few crosscompilers
|
||||
# available in the system.
|
||||
# Note that this implementation is mainly used for demonstration
|
||||
# purpose, therefore the hardcoded compiler names are used in
|
||||
# DEBIAN/Ubuntu Linux (as I only installed these ones at the
|
||||
# time of writing this example)
|
||||
available_targets = find_all_available_targets()
|
||||
|
||||
targets =
|
||||
case {only_local(), only_listed_targets(), current_target()} do
|
||||
{true, true, {:ok, current}} ->
|
||||
if Enum.member?(available_targets, current) do
|
||||
[current]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
{true, _, {:error, err_msg}} ->
|
||||
Mix.raise(err_msg)
|
||||
|
||||
{true, false, {:ok, current}} ->
|
||||
Enum.uniq([current] ++ available_targets)
|
||||
|
||||
{false, true, _} ->
|
||||
available_targets
|
||||
|
||||
{false, false, {:ok, current}} ->
|
||||
Enum.uniq([current] ++ available_targets)
|
||||
end
|
||||
|
||||
if exclude_current_target() do
|
||||
case current_target() do
|
||||
{:ok, current} ->
|
||||
targets -- [current]
|
||||
|
||||
_ ->
|
||||
targets
|
||||
end
|
||||
else
|
||||
targets
|
||||
end
|
||||
end
|
||||
|
||||
@impl ElixirMake.Precompiler
|
||||
def all_supported_targets(:fetch) do
|
||||
Enum.map(compilers(), fn {os, compilers} ->
|
||||
Enum.map(Map.keys(compilers), fn key ->
|
||||
if key == :include_default_ones do
|
||||
Map.keys(default_compilers()[os])
|
||||
else
|
||||
key
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|> List.flatten()
|
||||
end
|
||||
|
||||
@impl ElixirMake.Precompiler
|
||||
def unavailable_target(_) do
|
||||
if only_listed_targets() do
|
||||
:ignore
|
||||
else
|
||||
:compile
|
||||
end
|
||||
end
|
||||
|
||||
defp find_all_available_targets do
|
||||
compilers = compilers_current_os_with_override()
|
||||
|
||||
compilers
|
||||
|> Map.keys()
|
||||
|> Enum.map(&find_available_compilers(&1, Map.get(compilers, &1)))
|
||||
|> Enum.reject(fn x -> x == nil end)
|
||||
end
|
||||
|
||||
defp find_available_compilers(triplet, prefix) when is_binary(prefix) do
|
||||
if ensure_executable(["#{prefix}gcc", "#{prefix}g++"]) do
|
||||
Logger.debug("Found compiler for #{triplet}")
|
||||
triplet
|
||||
else
|
||||
Logger.debug("Compiler not found for #{triplet}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp find_available_compilers(triplet, {cc, cxx}) when is_binary(cc) and is_binary(cxx) do
|
||||
if ensure_executable([cc, cxx]) do
|
||||
Logger.debug("Found compiler for #{triplet}")
|
||||
triplet
|
||||
else
|
||||
Logger.debug("Compiler not found for #{triplet}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp find_available_compilers(triplet, {:script, _, _}) do
|
||||
triplet
|
||||
end
|
||||
|
||||
defp find_available_compilers(triplet, {cc_executable, cxx_executable, _, _})
|
||||
when is_binary(cc_executable) and is_binary(cxx_executable) do
|
||||
if ensure_executable([cc_executable, cxx_executable]) do
|
||||
Logger.debug("Found compiler for #{triplet}")
|
||||
triplet
|
||||
else
|
||||
Logger.debug("Compiler not found for #{triplet}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp find_available_compilers(triplet, invalid) do
|
||||
Mix.raise(
|
||||
"Invalid configuration for #{triplet}, expecting a string, 2-tuple or 4-tuple. Got `#{inspect(invalid)}`"
|
||||
)
|
||||
end
|
||||
|
||||
defp ensure_executable(executable_list) when is_list(executable_list) do
|
||||
if allow_missing_compiler() do
|
||||
Enum.any?(executable_list, &System.find_executable/1)
|
||||
else
|
||||
Enum.all?(executable_list, &System.find_executable/1)
|
||||
end
|
||||
end
|
||||
|
||||
@impl ElixirMake.Precompiler
|
||||
def build_native(args) do
|
||||
# In this callback we just build the NIF library natively,
|
||||
# and because this precompiler module is designed for NIF
|
||||
# libraries that use C/C++ as the main language with Makefile,
|
||||
# we can just call `ElixirMake.Precompiler.mix_compile(args)`
|
||||
#
|
||||
# It's also possible to forward this call to:
|
||||
#
|
||||
# `precompile(args, elem(current_target(), 1))`
|
||||
#
|
||||
# This could be useful when the precompiler is using a universal
|
||||
# (cross-)compiler, say zig. in this way, the compiled binaries
|
||||
# (`mix compile`) will be consistent as the corresponding precompiled
|
||||
# one (with `mix elixir_make.precompile`)
|
||||
#
|
||||
# However, if you'd prefer to having the same behaviour for `mix compile`
|
||||
# then the following line is okay
|
||||
ElixirMake.Precompiler.mix_compile(args)
|
||||
end
|
||||
|
||||
@impl ElixirMake.Precompiler
|
||||
def precompile(args, target) do
|
||||
# in this callback we compile the NIF library for a given target
|
||||
config = Mix.Project.config()
|
||||
app = config[:app]
|
||||
version = config[:version]
|
||||
priv_paths = config[:make_precompiler_priv_paths] || ["."]
|
||||
|
||||
saved_cc = System.get_env("CC") || ""
|
||||
saved_cxx = System.get_env("CXX") || ""
|
||||
saved_cpp = System.get_env("CPP") || ""
|
||||
|
||||
Logger.debug("Current compiling target: #{target}")
|
||||
|
||||
cc_cxx = get_cc_and_cxx(target)
|
||||
|
||||
# remove files in the lists
|
||||
app_priv = Path.join(Mix.Project.app_path(config), "priv")
|
||||
|
||||
case priv_paths do
|
||||
["."] ->
|
||||
File.rm_rf!(app_priv)
|
||||
|
||||
_ ->
|
||||
for include <- priv_paths,
|
||||
file <- Path.wildcard(Path.join(app_priv, include)) do
|
||||
File.rm_rf(file)
|
||||
end
|
||||
end
|
||||
|
||||
File.mkdir_p!(app_priv)
|
||||
|
||||
case cc_cxx do
|
||||
{cc, cxx} ->
|
||||
System.put_env("CC", cc)
|
||||
System.put_env("CXX", cxx)
|
||||
System.put_env("CPP", cxx)
|
||||
|
||||
System.put_env("CC_PRECOMPILER_CURRENT_TARGET", target)
|
||||
ElixirMake.Precompiler.mix_compile(args)
|
||||
|
||||
{:script, module, custom_args} ->
|
||||
System.put_env("CC_PRECOMPILER_CURRENT_TARGET", target)
|
||||
|
||||
Kernel.apply(module, :compile, [
|
||||
app,
|
||||
version,
|
||||
"#{:erlang.system_info(:nif_version)}",
|
||||
target,
|
||||
args,
|
||||
custom_args
|
||||
])
|
||||
end
|
||||
|
||||
System.put_env("CC", saved_cc)
|
||||
System.put_env("CXX", saved_cxx)
|
||||
System.put_env("CPP", saved_cpp)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp get_cc_and_cxx(triplet) do
|
||||
case Access.get(compilers_current_os_with_override(), triplet, nil) do
|
||||
nil ->
|
||||
cc = System.get_env("CC")
|
||||
cxx = System.get_env("CXX")
|
||||
cpp = System.get_env("CPP")
|
||||
|
||||
case {cc, cxx, cpp} do
|
||||
{nil, _, _} ->
|
||||
{"gcc", "g++"}
|
||||
|
||||
{_, nil, nil} ->
|
||||
{"gcc", "g++"}
|
||||
|
||||
{_, _, nil} ->
|
||||
{cc, cxx}
|
||||
|
||||
{_, nil, _} ->
|
||||
{cc, cpp}
|
||||
|
||||
{_, _, _} ->
|
||||
{cc, cxx}
|
||||
end
|
||||
|
||||
{cc, cxx} ->
|
||||
{cc, cxx}
|
||||
|
||||
prefix when is_binary(prefix) ->
|
||||
{"#{prefix}gcc", "#{prefix}g++"}
|
||||
|
||||
{:script, script_path, {module, args}} ->
|
||||
case {script_path, module} do
|
||||
{"", CCPrecompiler.UniversalBinary} ->
|
||||
{:script, module, args}
|
||||
|
||||
_ ->
|
||||
Code.require_file(script_path)
|
||||
{:script, module, args}
|
||||
end
|
||||
|
||||
{cc, cxx, cc_args, cxx_args} ->
|
||||
{EEx.eval_string(cc_args, cc: cc), EEx.eval_string(cxx_args, cxx: cxx)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def post_precompile_target(target) do
|
||||
config = Mix.Project.config()
|
||||
cc_precompiler_config = config[:cc_precompiler]
|
||||
cleanup(config, cc_precompiler_config[:cleanup], target)
|
||||
end
|
||||
|
||||
defp cleanup(_, nil, _), do: :ok
|
||||
|
||||
defp cleanup(config, make_target, current_precompilation_target) when is_binary(make_target) do
|
||||
exec =
|
||||
System.get_env("MAKE") ||
|
||||
os_specific_executable(Keyword.get(config, :make_executable, :default))
|
||||
|
||||
makefile = Keyword.get(config, :make_makefile, :default)
|
||||
env = Keyword.get(config, :make_env, %{})
|
||||
env = if is_function(env), do: env.(), else: env
|
||||
env = default_env(config, env, current_precompilation_target)
|
||||
|
||||
# In OTP 19, Erlang's `open_port/2` ignores the current working
|
||||
# directory when expanding relative paths. This means that `:make_cwd`
|
||||
# must be an absolute path. This is a different behaviour from earlier
|
||||
# OTP versions and appears to be a bug. It is being tracked at
|
||||
# https://bugs.erlang.org/browse/ERL-175.
|
||||
cwd = Keyword.get(config, :make_cwd, ".") |> Path.expand(File.cwd!())
|
||||
|
||||
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) ++ [make_target]
|
||||
|
||||
case cmd(exec, args, cwd, env) do
|
||||
0 ->
|
||||
:ok
|
||||
|
||||
exit_status ->
|
||||
raise_cleanup_error(exec, exit_status)
|
||||
end
|
||||
end
|
||||
|
||||
defp raise_cleanup_error(exec, exit_status) do
|
||||
Mix.raise(~s{Could not complete cleanup work with "#{exec}" (exit status: #{exit_status}).\n})
|
||||
end
|
||||
|
||||
# Returns a map of default environment variables
|
||||
# Defaults may be overwritten.
|
||||
defp default_env(config, default_env, current_precompilation_target) 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,
|
||||
|
||||
# cc_precompiler
|
||||
"CC_PRECOMPILER_CURRENT_TARGET" => current_precompilation_target
|
||||
},
|
||||
default_env
|
||||
)
|
||||
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] ->
|
||||
"gmake"
|
||||
|
||||
_ ->
|
||||
"make"
|
||||
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]
|
||||
|
||||
# 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) do
|
||||
opts = [
|
||||
into: IO.stream(:stdio, :line),
|
||||
stderr_to_stdout: true,
|
||||
cd: cwd,
|
||||
env: env
|
||||
]
|
||||
|
||||
{%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 env(var, default) do
|
||||
System.get_env(var) || default
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user