diff --git a/PRECOMPILATION_GUIDE.md b/PRECOMPILATION_GUIDE.md index c4099ab..8ed891f 100644 --- a/PRECOMPILATION_GUIDE.md +++ b/PRECOMPILATION_GUIDE.md @@ -105,17 +105,49 @@ Directory structures and symbolic links are preserved. #### `make_precompiler_nif_versions` (optional config key) -The third optional config key is `make_precompiler_nif_versions`. The default value is +The third optional config key is `make_precompiler_nif_versions`, which configures `elixir_make` on how to compile and reuse precompiled binaries. + +The default value for `make_precompiler_nif_versions` is ```elixir [versions: ["#{:erlang.system_info(:nif_version)}"]] ``` -If you'd like to aim for an older NIF version, say `2.15` for Erlang/OTP 23 and 24, then you need to setup CI correspondingly and set the value of this key to `[versions: ["2.15", "2.16"]]`. This optional key will only be checked when downloading precompiled artefacts. +There're three sub-keys for `make_precompiler_nif_versions`: + +- `versions` +- `fallback_version` +- `availability` + +##### `versions` sub-key + +The `versions` sub-key is a list of NIF versions that the precompiled artefacts are available for: + +```elixir +make_precompiler_nif_versions: [ + versions: ["2.15", "2.16"] +] +``` + +The default behaviour is to use the exact NIF version that is available to the current target. If one is not available, it may fallback (see `fallback_version` next) to the highest matching major version prior to the current version. For example: + +- if the current host is using Erlang/OTP 23 (NIF version `2.15`), `elixir_make` will use the precompiled artefacts for NIF version `2.15`; +- if the current host is using Erlang/OTP 24 or 25 (NIF version `2.16`), `elixir_make` will use the precompiled artefacts for NIF version `2.16`; +- if the current host is using Erlang/OTP 26 or newer (NIF version `2.17`), `elixir_make` will fallback to the precompiled artefacts for NIF version `2.16`; + +If the current host is using Erlang/OTP with a new major Erlang NIF version (NIF version `3.0`) or anything earlier than the precompiled versions (`2.14`), `elixir_make` will compile from scratch. + +##### `fallback_version` sub-key + +The behaviour when `elixir_make` cannot find the exact NIF version of the precompiled binary can be customized by setting the `fallback_version` sub-key. The value of the `fallback_version` sub-key should be a function that accepts three arguments, `target`, `current_nif_version` and `target_versions`. The `target` is the target triplet (or other name format, defined by the precompiler of your choice), `current_nif_version` is the NIF version on the current host, and `target_versions` is a list of NIF versions that are available to the target. + +The `fallback_version` function should return either the NIF version that `elixir_make` should use from the `target_versions` list or the `current_nif_version`. + +##### `availability` sub-key For some platforms maybe we only have precompiled artefacts after a certain NIF version, say for x86_64 Windows we have precompiled artefacts available when NIF version >= `2.16` while other platforms have precompiled artefacts available from NIF version >= `2.15`. -In such case we can inform `:elixir_make` that Windows targets don't have precompiled artefacts available except for NIF version `2.16` by passing a function to the `availability` sub-key. +In such case we can inform `:elixir_make` that Windows targets don't have precompiled artefacts available except for NIF version `2.16` by passing a function to the `availability` sub-key. This function should accept two arguments, `target` and `nif_version`, and returns a boolean value indicating whether the precompiled artefacts for the target and NIF version are available. ```elixir defp target_available_for_nif_version?(target, nif_version) do diff --git a/lib/elixir_make/artefact.ex b/lib/elixir_make/artefact.ex index 3e5d273..ca84591 100644 --- a/lib/elixir_make/artefact.ex +++ b/lib/elixir_make/artefact.ex @@ -141,6 +141,28 @@ defmodule ElixirMake.Artefact do ## Archive/NIF urls + defp nif_version_to_tuple(nif_version) do + [major, minor | _] = String.split(nif_version, ".") + {String.to_integer(major), String.to_integer(minor)} + end + + defp fallback_version(_current_target, current_nif_version, versions) do + {major, minor} = nif_version_to_tuple(current_nif_version) + + # Get all matching major versions, earlier than the current version + # and their distance. We want the closest (smallest distance). + candidates = + for version <- versions, + {^major, candidate_minor} <- [nif_version_to_tuple(version)], + candidate_minor <= minor, + do: {minor - candidate_minor, version} + + case Enum.sort(candidates) do + [{_, version} | _] -> version + _ -> current_nif_version + end + end + @doc """ Returns all available {{target, nif_version}, url} pairs available. """ @@ -151,27 +173,31 @@ defmodule ElixirMake.Artefact do config[:make_precompiler_url] || Mix.raise("`make_precompiler_url` is not specified in `project`") + current_nif_version = "#{:erlang.system_info(:nif_version)}" + nif_versions = config[:make_precompiler_nif_versions] || - [versions: ["#{:erlang.system_info(:nif_version)}"]] + [versions: [current_nif_version]] + + versions = nif_versions[:versions] Enum.reduce(targets, [], fn target, archives -> archive_filenames = - Enum.reduce(nif_versions[:versions], [], fn nif_version, acc -> + Enum.reduce(versions, [], fn nif_version_for_target, acc -> availability = nif_versions[:availability] available? = if is_function(availability, 2) do - availability.(target, nif_version) + availability.(target, nif_version_for_target) else true end if available? do - archive_filename = archive_filename(config, target, nif_version) + archive_filename = archive_filename(config, target, nif_version_for_target) [ - {{target, nif_version}, + {{target, nif_version_for_target}, String.replace(url_template, "@{artefact_filename}", archive_filename)} | acc ] @@ -187,11 +213,25 @@ defmodule ElixirMake.Artefact do @doc """ Returns the url for the current target. """ - def current_target_url(config, precompiler, nif_version) do + def current_target_url(config, precompiler, current_nif_version) do case precompiler.current_target() do {:ok, current_target} -> + nif_versions = + config[:make_precompiler_nif_versions] || + [versions: []] + + versions = nif_versions[:versions] + + nif_version_to_use = + if current_nif_version in versions do + current_nif_version + else + fallback_version = nif_versions[:fallback_version] || (&fallback_version/3) + fallback_version.(current_target, current_nif_version, versions) + end + available_urls = available_target_urls(config, precompiler) - target_at_nif_version = {current_target, nif_version} + target_at_nif_version = {current_target, nif_version_to_use} case List.keyfind(available_urls, target_at_nif_version, 0) do {^target_at_nif_version, download_url} -> diff --git a/lib/mix/tasks/elixir_make.checksum.ex b/lib/mix/tasks/elixir_make.checksum.ex index 088f915..dea63de 100644 --- a/lib/mix/tasks/elixir_make.checksum.ex +++ b/lib/mix/tasks/elixir_make.checksum.ex @@ -54,9 +54,11 @@ defmodule Mix.Tasks.ElixirMake.Checksum do Artefact.available_target_urls(config, precompiler) Keyword.get(options, :only_local) -> - case Artefact.current_target_url(config, precompiler, :erlang.system_info(:nif_version)) do + current_nif_version = "#{:erlang.system_info(:nif_version)}" + + case Artefact.current_target_url(config, precompiler, current_nif_version) do {:ok, target, url} -> - [{{target, "#{:erlang.system_info(:nif_version)}"}, url}] + [{{target, current_nif_version}, url}] {:error, {:unavailable_target, current_target, error}} -> recover =