Skip to content

Allow libraries to reuse binaries compiled with older NIF versions #79

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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