Skip to content

Commit

Permalink
Allow libraries to reuse binaries compiled with older NIF versions (#79)
Browse files Browse the repository at this point in the history
  • Loading branch information
cocoa-xu committed Mar 9, 2024
1 parent 5c5fffc commit 43f29f8
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 12 deletions.
38 changes: 35 additions & 3 deletions PRECOMPILATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 47 additions & 7 deletions lib/elixir_make/artefact.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand All @@ -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
]
Expand All @@ -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} ->
Expand Down
6 changes: 4 additions & 2 deletions lib/mix/tasks/elixir_make.checksum.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down

0 comments on commit 43f29f8

Please sign in to comment.