lib/compilation_script/universal_binary.ex

defmodule CCPrecompiler.UniversalBinary do
  @moduledoc """
  Build a universal binary on macOS

  ## Example

    "macos-universal" => {
      :script, "", {CCPrecompiler.UniversalBinary, []}
    }

  """

  @behaviour CCPrecompiler.CompilationScript

  @impl CCPrecompiler.CompilationScript
  def compile(_app, _version, _nif_version, _target, args, _custom_args) do
    config = Mix.Project.config()
    app_priv = Path.join(Mix.Project.app_path(config), "priv")
    make_precompiler_filename = config[:make_precompiler_filename] || "nif"
    nif_file = "#{make_precompiler_filename}.so"

    compiled_bin = Path.join(app_priv, nif_file)
    x86_64_bin = Path.join(app_priv, "#{make_precompiler_filename}_x86_64.so")
    aarch64_bin = Path.join(app_priv, "#{make_precompiler_filename}_aarch64.so")

    File.rm(compiled_bin)

    # first we compile `x86_64-apple-darwin`
    :ok = System.put_env("CC", "gcc -arch x86_64")
    System.put_env("CXX", "gcc -arch x86_64")
    System.put_env("CPP", "g++ -arch x86_64")
    ElixirMake.Compiler.compile(args)
    File.rename!(compiled_bin, x86_64_bin)

    # then we compile `aarch64-apple-darwin`
    System.put_env("CC", "gcc -arch arm64")
    System.put_env("CXX", "gcc -arch arm64")
    System.put_env("CPP", "g++ -arch arm64")
    ElixirMake.Compiler.compile(args)
    File.rename!(compiled_bin, aarch64_bin)

    {_, exit_status} =
      System.cmd("lipo", ["-create", "-output", compiled_bin, x86_64_bin, aarch64_bin])

    File.rm!(x86_64_bin)
    File.rm!(aarch64_bin)

    if exit_status == 0 do
      :ok
    else
      Mix.raise("Failed to create universal binary")
    end
  end
end