lib/util/default_erts_resolver.ex

defmodule Burrito.Util.DefaultERTSResolver do
  alias Burrito.Builder.Target
  alias Burrito.Builder.Log

  alias Burrito.Util
  alias Burrito.Util.FileCache
  alias Burrito.Util.ERTSResolver
  alias Burrito.Util.ERTSUniversalMachineFetcher

  @behaviour ERTSResolver

  @impl ERTSResolver
  @spec do_resolve(Burrito.Builder.Target.t()) :: Burrito.Builder.Target.t()
  def do_resolve(%Target{erts_source: {:runtime, _}} = target) do
    %Target{target | erts_source: {:runtime, version: Util.get_otp_version()}}
  end

  def do_resolve(%Target{erts_source: {:precompiled, version: otp_version}} = target)
      when is_binary(otp_version) do
    case ERTSUniversalMachineFetcher.fetch_version(
           target.os,
           target.qualifiers[:libc],
           target.cpu,
           otp_version
         ) do
      %URI{} = location ->
        %Target{target | erts_source: {:url, url: location}} |> do_resolve()
    end
  end

  def do_resolve(%Target{erts_source: {:local, path: location}} = target) do
    archive_data = File.read!(location)
    unpacked_location = do_unpack(archive_data, target)

    %Target{target | erts_source: {:local_unpacked, path: unpacked_location}}
  end

  def do_resolve(%Target{erts_source: {:url, url: location}} = target) do
    url_string = URI.to_string(location)
    archive_data = get_erts(url_string)
    unpacked_location = do_unpack(archive_data, target)

    %Target{target | erts_source: {:local_unpacked, path: unpacked_location}}
  end

  def do_resolve(%Target{erts_source: {:local_unpacked, path: _location}} = target) do
    target
  end

  defp get_erts(tar_url) do
    cache_key = :crypto.hash(:sha, tar_url) |> Base.encode16()

    case FileCache.fetch(cache_key) do
      {:hit, data} ->
        Log.info(:step, "Found matching cached ERTS, using that")
        data

      _ ->
        do_download(tar_url, cache_key)
    end
  end

  defp do_unpack(data, %Target{} = target) when is_binary(data) do
    # save the payload somewhere
    random_id = :crypto.strong_rand_bytes(8) |> Base.encode16()
    tar_dest_path = System.tmp_dir!() |> Path.join(["erts_#{random_id}"])
    File.write!(tar_dest_path, data)

    random_id = :crypto.strong_rand_bytes(8) |> Base.encode16()
    extraction_path = System.tmp_dir!() |> Path.join(["unpacked_erts_#{random_id}"])
    File.mkdir_p!(extraction_path)

    # we use 7z to unpack windows setup files, otherwise we use tar
    command =
      case target.os do
        :windows -> ~c"7z x #{tar_dest_path} -o#{extraction_path}/otp-windows/"
        _ -> ~c"tar xzf #{tar_dest_path} -C #{extraction_path}"
      end

    :os.cmd(command)

    File.rm!(tar_dest_path)

    Log.info(:step, "Unpacked ERTS to: #{extraction_path}")

    extraction_path
  end

  defp do_download(url, cache_key) do
    {:ok, _} = Application.ensure_all_started(:req)
    Log.info(:step, "Downloading file: #{url}")
    resp = Req.get!(url, raw: true)

    if resp.status != 200 do
      raise "Failed to fetch #{url}! (Got #{resp.status}) Perhaps we haven't built a pre-compiled Erlang for this release yet? If this was a 404, please file an issue! Thanks!"
    end

    FileCache.put_if_not_exist(cache_key, resp.body)
    resp.body
  end
end