Skip to content

Commit 43f29f8

Browse files
authored
Allow libraries to reuse binaries compiled with older NIF versions (#79)
1 parent 5c5fffc commit 43f29f8

File tree

3 files changed

+86
-12
lines changed

3 files changed

+86
-12
lines changed

PRECOMPILATION_GUIDE.md

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,49 @@ Directory structures and symbolic links are preserved.
105105

106106
#### `make_precompiler_nif_versions` (optional config key)
107107

108-
The third optional config key is `make_precompiler_nif_versions`. The default value is
108+
The third optional config key is `make_precompiler_nif_versions`, which configures `elixir_make` on how to compile and reuse precompiled binaries.
109+
110+
The default value for `make_precompiler_nif_versions` is
109111

110112
```elixir
111113
[versions: ["#{:erlang.system_info(:nif_version)}"]]
112114
```
113115

114-
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.
116+
There're three sub-keys for `make_precompiler_nif_versions`:
117+
118+
- `versions`
119+
- `fallback_version`
120+
- `availability`
121+
122+
##### `versions` sub-key
123+
124+
The `versions` sub-key is a list of NIF versions that the precompiled artefacts are available for:
125+
126+
```elixir
127+
make_precompiler_nif_versions: [
128+
versions: ["2.15", "2.16"]
129+
]
130+
```
131+
132+
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:
133+
134+
- 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`;
135+
- 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`;
136+
- 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`;
137+
138+
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.
139+
140+
##### `fallback_version` sub-key
141+
142+
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.
143+
144+
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`.
145+
146+
##### `availability` sub-key
115147

116148
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`.
117149

118-
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.
150+
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.
119151

120152
```elixir
121153
defp target_available_for_nif_version?(target, nif_version) do

lib/elixir_make/artefact.ex

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,28 @@ defmodule ElixirMake.Artefact do
141141

142142
## Archive/NIF urls
143143

144+
defp nif_version_to_tuple(nif_version) do
145+
[major, minor | _] = String.split(nif_version, ".")
146+
{String.to_integer(major), String.to_integer(minor)}
147+
end
148+
149+
defp fallback_version(_current_target, current_nif_version, versions) do
150+
{major, minor} = nif_version_to_tuple(current_nif_version)
151+
152+
# Get all matching major versions, earlier than the current version
153+
# and their distance. We want the closest (smallest distance).
154+
candidates =
155+
for version <- versions,
156+
{^major, candidate_minor} <- [nif_version_to_tuple(version)],
157+
candidate_minor <= minor,
158+
do: {minor - candidate_minor, version}
159+
160+
case Enum.sort(candidates) do
161+
[{_, version} | _] -> version
162+
_ -> current_nif_version
163+
end
164+
end
165+
144166
@doc """
145167
Returns all available {{target, nif_version}, url} pairs available.
146168
"""
@@ -151,27 +173,31 @@ defmodule ElixirMake.Artefact do
151173
config[:make_precompiler_url] ||
152174
Mix.raise("`make_precompiler_url` is not specified in `project`")
153175

176+
current_nif_version = "#{:erlang.system_info(:nif_version)}"
177+
154178
nif_versions =
155179
config[:make_precompiler_nif_versions] ||
156-
[versions: ["#{:erlang.system_info(:nif_version)}"]]
180+
[versions: [current_nif_version]]
181+
182+
versions = nif_versions[:versions]
157183

158184
Enum.reduce(targets, [], fn target, archives ->
159185
archive_filenames =
160-
Enum.reduce(nif_versions[:versions], [], fn nif_version, acc ->
186+
Enum.reduce(versions, [], fn nif_version_for_target, acc ->
161187
availability = nif_versions[:availability]
162188

163189
available? =
164190
if is_function(availability, 2) do
165-
availability.(target, nif_version)
191+
availability.(target, nif_version_for_target)
166192
else
167193
true
168194
end
169195

170196
if available? do
171-
archive_filename = archive_filename(config, target, nif_version)
197+
archive_filename = archive_filename(config, target, nif_version_for_target)
172198

173199
[
174-
{{target, nif_version},
200+
{{target, nif_version_for_target},
175201
String.replace(url_template, "@{artefact_filename}", archive_filename)}
176202
| acc
177203
]
@@ -187,11 +213,25 @@ defmodule ElixirMake.Artefact do
187213
@doc """
188214
Returns the url for the current target.
189215
"""
190-
def current_target_url(config, precompiler, nif_version) do
216+
def current_target_url(config, precompiler, current_nif_version) do
191217
case precompiler.current_target() do
192218
{:ok, current_target} ->
219+
nif_versions =
220+
config[:make_precompiler_nif_versions] ||
221+
[versions: []]
222+
223+
versions = nif_versions[:versions]
224+
225+
nif_version_to_use =
226+
if current_nif_version in versions do
227+
current_nif_version
228+
else
229+
fallback_version = nif_versions[:fallback_version] || (&fallback_version/3)
230+
fallback_version.(current_target, current_nif_version, versions)
231+
end
232+
193233
available_urls = available_target_urls(config, precompiler)
194-
target_at_nif_version = {current_target, nif_version}
234+
target_at_nif_version = {current_target, nif_version_to_use}
195235

196236
case List.keyfind(available_urls, target_at_nif_version, 0) do
197237
{^target_at_nif_version, download_url} ->

lib/mix/tasks/elixir_make.checksum.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,11 @@ defmodule Mix.Tasks.ElixirMake.Checksum do
5454
Artefact.available_target_urls(config, precompiler)
5555

5656
Keyword.get(options, :only_local) ->
57-
case Artefact.current_target_url(config, precompiler, :erlang.system_info(:nif_version)) do
57+
current_nif_version = "#{:erlang.system_info(:nif_version)}"
58+
59+
case Artefact.current_target_url(config, precompiler, current_nif_version) do
5860
{:ok, target, url} ->
59-
[{{target, "#{:erlang.system_info(:nif_version)}"}, url}]
61+
[{{target, current_nif_version}, url}]
6062

6163
{:error, {:unavailable_target, current_target, error}} ->
6264
recover =

0 commit comments

Comments
 (0)