Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
261 changes: 216 additions & 45 deletions PRECOMPILATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,82 @@ Of course, wildcards (`?`, `**`, `*`) are supported when specifiying files. For

Directory structures and symbolic links are preserved.

### (Optional) Customise Precompilation Targets

To override the default configuration, please set the `cc_precompile` key in `project`. For example,

```elixir

def project do
[
# ...
cc_precompile: [
# optional config that provides a map of available compilers
# on different systems
compilers: %{
# key (`:os.type()`)
# this allows us to provide different available targets
# on different systems
# value is a map that describes which compilers are available
#
# key == {:unix, :linux} => when compiling on Linux
{:unix, :linux} => %{
# key (target triplet) => `riscv64-linux-gnu`
# value => `PREFIX`
# - for strings, the string will be used as the prefix of
# the C and C++ compiler respectively, i.e.,
# CC=`#{prefix}gcc`
# CXX=`#{prefix}g++`
"riscv64-linux-gnu" => "riscv64-linux-gnu-",
# key (target triplet) => `armv7l-linux-gnueabihf`
# value => `{CC, CXX}`
# - for 2-tuples, the elements are the executable name of
# the C and C++ compiler respectively
"armv7l-linux-gnueabihf" => {
"arm-linux-gnueabihf-gcc",
"arm-linux-gnueabihf-g++"
},
# key (target triplet) => `armv7l-linux-gnueabihf`
# value => `{CC_EXECUTABLE, CXX_EXECUTABLE, CC_TEMPLATE, CXX_TEMPLATE}`
#
# - for 4-tuples, the first two elements are the same as in
# 2-tuple, the third and fourth elements are the template
# string for CC and CPP/CXX. for example,
#
# the last entry below shows the example of using zig as the
# crosscompiler for `aarch64-linux-musl`,
# the "CC" will be
# "zig cc -target aarch64-linux-musl",
# and "CXX" and "CPP" will be
# "zig c++ -target aarch64-linux-musl"
"aarch64-linux-musl" => {
"zig",
"zig",
"<% cc %> cc -target aarch64-linux-musl",
"<% cxx %> c++ -target aarch64-linux-musl"
}
},
# key == {:unix, :darwin} => when compiling on macOS
{:unix, :darwin} => %{
# key (target triplet) => `aarch64-apple-darwin`
# value => `{CC, CXX}`
"aarch64-apple-darwin" => {
"gcc -arch arm64", "g++ -arch arm64"
},
# key (target triplet) => `aarch64-linux-musl`
# value => `{CC_EXECUTABLE, CXX_EXECUTABLE, CC_TEMPLATE, CXX_TEMPLATE}`
"aarch64-linux-musl" => {
"zig",
"zig",
"<% cc %> cc -target aarch64-linux-musl",
"<% cxx %> c++ -target aarch64-linux-musl"
}
}
}
]
]
```

### (Optional) Test the NIF code locally

To test the NIF code locally, you can either set `force_build` to `true` or append `"-dev"` to your NIF library's version string.
Expand Down Expand Up @@ -225,7 +301,7 @@ defp package do
[
files: [
"lib",
"checksum-*.exs",
"checksum.exs",
"mix.exs",
# ...
],
Expand Down Expand Up @@ -370,69 +446,139 @@ defmodule CCPrecompiler do

# This is the default configuration for this demo precompiler module
# for 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
# - arm-linux-gnueabihf
# - 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
# - gcc-arm-linux-gnueabihf
# (this demo module will only try to find the CC executable, a step further
# - 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} => %{
"aarch64-linux-gnu" => {"aarch64-linux-gnu-gcc", "aarch64-linux-gnu-g++"},
"riscv64-linux-gnu" => {"riscv64-linux-gnu-gcc", "riscv64-linux-gnu-g++"},
"arm-linux-gnueabihf" => {"gcc-arm-linux-gnueabihf", "g++-arm-linux-gnueabihf"},
"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++", "-arch x86_64", "-arch x86_64"
"gcc",
"g++",
"<%= cc %> -arch x86_64",
"<%= cxx %> -arch x86_64"
},
"aarch64-apple-darwin" => {
"gcc", "g++", "-arch arm64", "-arch arm64"
"gcc",
"g++",
"<%= cc %> -arch arm64",
"<%= cxx %> -arch arm64"
}
},
{:win32, :nt} => %{
"x86_64-windows-msvc" => {"cl", "cl"}
}
}

@user_config Application.compile_env(Mix.Project.config[:app], :cc_precompile)
@compilers Access.get(@user_config, :compilers, @default_compilers)
@compilers_current_os Access.get(@compilers, :os.type(), %{})
defp default_compilers, do: @default_compilers
defp user_config, do: Mix.Project.config()[:cc_precompile] || default_compilers()
defp compilers, do: Access.get(user_config(), :compilers, default_compilers())
defp compilers_current_os, do: Access.get(compilers(), :os.type(), %{})

@impl ElixirMake.Precompiler
def current_target do
current_target_user_overwrite = Access.get(@user_config, :current_target)
current_target_from_env = current_target_from_env()

if current_target_user_overwrite do
if current_target_from_env do
# overwrite current target triplet
{:ok, current_target_user_overwrite}
{:ok, current_target_from_env}
else
# 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
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-"

_ ->
{:error, "cannot decide current target"}
"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

Expand All @@ -457,13 +603,12 @@ defmodule CCPrecompiler do

@impl ElixirMake.Precompiler
def all_supported_targets(:fetch) do
Enum.flat_map(@compilers, &Map.keys(elem(&1, 1)))
Enum.flat_map(compilers(), &Map.keys(elem(&1, 1)))
end

defp find_all_available_targets do
@compilers_current_os
|> Map.keys()
|> Enum.map(&find_available_compilers(&1, Map.get(@compilers_current_os, &1)))
Map.keys(compilers_current_os())
|> Enum.map(&find_available_compilers(&1, Map.get(compilers_current_os(), &1)))
|> Enum.reject(&is_nil/1)
end

Expand Down Expand Up @@ -527,10 +672,36 @@ defmodule CCPrecompiler do
:ok
end

defp get_cc_and_cxx(triplet, default \\ {"gcc", "g++"}) do
case Access.get(@compilers_current_os, triplet, default) do
defp get_cc_and_cxx(triplet) do
case Access.get(compilers_current_os(), 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++"}

{cc, cxx, cc_args, cxx_args} ->
{"#{cc} #{cc_args}", "#{cxx} #{cxx_args}"}
end
Expand Down