defmodule Burrito.Steps.Patch.RecompileNIFs do alias Burrito.Builder.Context alias Burrito.Builder.Log alias Burrito.Builder.Step alias Burrito.Builder.Target @behaviour Step @impl Step def execute(%Context{} = context) do cflags = Keyword.get(context.target.qualifiers, :nif_cflags, "") cxxflags = Keyword.get(context.target.qualifiers, :nif_cxxflags, "") nif_env = Keyword.get(context.target.qualifiers, :nif_env, []) nif_make_args = Keyword.get(context.target.qualifiers, :nif_make_args, []) skip_nifs? = Keyword.get(context.target.qualifiers, :skip_nifs, false) if context.target.cross_build and not skip_nifs? do triplet = Target.make_triplet(context.target) {:local_unpacked, path: erts_location} = context.target.erts_source nif_sniff() |> Enum.each(fn dep -> maybe_recompile_nif(dep, context.work_dir, erts_location, triplet, cflags, cxxflags, nif_env, nif_make_args) end) end context end def nif_sniff() do # The current procedure for finding out if a dependency has a NIF: # - List all deps in the project using Mix.Project.deps_paths/0 # - Iterate over those, and use Mix.Project.in_project/4 to execute a function inside their project context # - Check if they contain :elixir_make in their `:compilers` # # We'll probably need to expand how we detect NIFs, but :elixir_make is a popular way to compile NIFs # so it's a good place to start... paths = Mix.Project.deps_paths() |> Enum.filter(fn {name, _} -> name != :burrito end) Enum.map(paths, fn {dep_name, path} -> Mix.Project.in_project(dep_name, path, fn module -> if module && Keyword.has_key?(module.project, :compilers) do {dep_name, path, Enum.member?(module.project[:compilers], :elixir_make)} else {dep_name, path, false} end end) end) end defp maybe_recompile_nif({_, _, false}, _, _, _, _, _, _, _), do: :no_nif defp maybe_recompile_nif( {dep, path, true}, release_working_path, erts_path, cross_target, extra_cflags, extra_cxxflags, extra_env, extra_make_args ) do dep = Atom.to_string(dep) Log.info(:step, "Going to recompile NIF for cross-build: #{dep} -> #{cross_target}") output_priv_dir = Path.join(release_working_path, ["lib/#{dep}*/"]) |> Path.expand() |> Path.wildcard() |> List.first() _ = System.cmd("make", ["clean"], cd: path, stderr_to_stdout: true, into: IO.stream()) # Compose env variables for cross-compilation, if we're building for linux, force dynamic linking erts_env = if String.contains?(cross_target, "linux") do erts_make_env(erts_path) ++ [{"LDFLAGS", "-dynamic-linker /dev/null"}] else erts_make_env(erts_path) end # This currently is only designed for elixir_make NIFs build_result = System.cmd("make", ["all", "--always-make"] ++ extra_make_args, cd: path, stderr_to_stdout: true, env: [ {"MIX_APP_PATH", output_priv_dir}, {"RANLIB", "zig ranlib"}, {"AR", "zig ar"}, {"CC", "zig cc -target #{cross_target} -O2 -dynamic -shared -Wl,-undefined=dynamic_lookup #{extra_cflags}"}, {"CXX", "zig c++ -target #{cross_target} -O2 -dynamic -shared -Wl,-undefined=dynamic_lookup #{extra_cxxflags}"} ] ++ erts_env ++ extra_env, into: IO.stream() ) case build_result do {_, 0} -> Log.info(:step, "Successfully re-built #{dep} for #{cross_target}!") src_priv_files = Path.join(output_priv_dir, ["priv/*"]) |> Path.expand() |> Path.wildcard() final_output_priv_dir = Path.join(output_priv_dir, "priv") Enum.each(src_priv_files, fn file -> file_name = Path.basename(file) if Path.extname(file_name) == ".so" && String.contains?(cross_target, "windows") do new_file_name = String.replace_trailing(file_name, ".so", ".dll") dst_fullpath = Path.join(final_output_priv_dir, new_file_name) Log.info(:step, "#{file} -> #{dst_fullpath}") File.rename!(file, dst_fullpath) else file_name end end) {output, _} -> Log.error(:step, "Failed to rebuild #{dep} for #{cross_target}!") Log.error(:step, output) exit(1) end end defp erts_make_env(erts_path) do ei_include = Path.join(erts_path, ["otp*/", "usr/", "include/"]) |> Path.expand() |> Path.wildcard() |> List.first() ei_lib = Path.join(erts_path, ["otp*/", "usr/", "lib/"]) |> Path.expand() |> Path.wildcard() |> List.first() erts_include = Path.join(erts_path, ["otp*/", "erts*/", "include/"]) |> Path.expand() |> Path.wildcard() |> List.first() [ {"ERL_EI_INCLUDE_DIR", ei_include}, {"ERL_EI_LIBDIR", ei_lib}, {"ERL_INTERFACE_INCLUDE_DIR", ei_include}, {"ERL_INTERFACE_LIB_DIR", ei_lib}, {"ERTS_INCLUDE_DIR", erts_include} ] end end