Skip to content

Commit 3044594

Browse files
authored
Add support for variants targets (#61)
* Make build of metadata save the variants This is needed in order to load the correct variants and building the list of artifacts for download. * Validates variants in the config * Add variants to the "available_nif_urls" function This makes the mix task know that needs to download the variants as well. * Testing for available NIF urls * Fix mix task that downloads artifacts for current target * Fix with suggestions from PR
1 parent 3138af8 commit 3044594

File tree

5 files changed

+346
-19
lines changed

5 files changed

+346
-19
lines changed

lib/mix/tasks/rustler_precompiled.download.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ defmodule Mix.Tasks.RustlerPrecompiled.Download do
3939
RustlerPrecompiled.available_nif_urls(module)
4040

4141
Keyword.get(options, :only_local) ->
42-
[RustlerPrecompiled.current_target_nif_url(module)]
42+
RustlerPrecompiled.current_target_nif_urls(module)
4343

4444
true ->
4545
raise "you need to specify either \"--all\" or \"--only-local\" flags"

lib/rustler_precompiled.ex

Lines changed: 108 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@ defmodule RustlerPrecompiled do
6666
* `:max_retries` - The maximum of retries before giving up. Defaults to `3`.
6767
Retries can be disabled with `0`.
6868
69+
* `:variants` - A map with alternative versions of a given target. This is useful to
70+
support specific versions of dependencies, such as an old glibc version, or to support
71+
restrict CPU features, like AVX on x86_64.
72+
73+
The order of variants matters, because the first one that returns `true` is going to be
74+
selected. Example:
75+
76+
%{"x86_64-unknown-linux-gnu" => [old_glibc: fn _config -> has_old_glibc?() end]}
77+
6978
In case "force build" is used, all options except `:base_url`, `:version`,
7079
`:force_build`, `:nif_versions`, and `:targets` are going to be passed down to `Rustler`.
7180
So if you need to configure the build, check the `Rustler` options.
@@ -180,7 +189,8 @@ defmodule RustlerPrecompiled do
180189
:force_build,
181190
:targets,
182191
:nif_versions,
183-
:max_retries
192+
:max_retries,
193+
:variants
184194
])
185195

186196
{:force_build, rustler_opts}
@@ -225,11 +235,23 @@ defmodule RustlerPrecompiled do
225235
is stored in a metadata file.
226236
"""
227237
def available_nif_urls(nif_module) when is_atom(nif_module) do
228-
metadata =
229-
nif_module
230-
|> metadata_file()
231-
|> read_map_from_file()
238+
nif_module
239+
|> metadata_file()
240+
|> read_map_from_file()
241+
|> nif_urls_from_metadata()
242+
|> case do
243+
{:ok, urls} ->
244+
urls
245+
246+
{:error, wrong_meta} ->
247+
raise "metadata about current target for the module #{inspect(nif_module)} is not available. " <>
248+
"Please compile the project again with: `mix compile --force` " <>
249+
"Metadata found: #{inspect(wrong_meta, limit: :infinity, pretty: true)}"
250+
end
251+
end
232252

253+
@doc false
254+
def nif_urls_from_metadata(metadata) when is_map(metadata) do
233255
case metadata do
234256
%{
235257
targets: targets,
@@ -238,42 +260,73 @@ defmodule RustlerPrecompiled do
238260
nif_versions: nif_versions,
239261
version: version
240262
} ->
241-
for target_triple <- targets, nif_version <- nif_versions do
242-
target = "nif-#{nif_version}-#{target_triple}"
263+
all_tar_gzs =
264+
for target_triple <- targets, nif_version <- nif_versions do
265+
target = "nif-#{nif_version}-#{target_triple}"
243266

244-
# We need to build again the name because each arch is different.
245-
lib_name = "#{lib_prefix(target)}#{basename}-v#{version}-#{target}"
267+
# We need to build again the name because each arch is different.
268+
lib_name = "#{lib_prefix(target)}#{basename}-v#{version}-#{target}"
269+
file_name = lib_name_with_ext(target_triple, lib_name)
246270

247-
tar_gz_file_url(base_url, lib_name_with_ext(target, lib_name))
248-
end
271+
tar_gz_urls(base_url, file_name, target_triple, metadata[:variants])
272+
end
249273

250-
_ ->
251-
raise "metadata about current target for the module #{inspect(nif_module)} is not available. " <>
252-
"Please compile the project again with: `mix compile --force`"
274+
{:ok, List.flatten(all_tar_gzs)}
275+
276+
wrong_meta ->
277+
{:error, wrong_meta}
253278
end
254279
end
255280

281+
defp maybe_variants_tar_gz_urls(nil, _, _, _), do: []
282+
283+
defp maybe_variants_tar_gz_urls(variants, base_url, target_triple, lib_name)
284+
when is_map_key(variants, target_triple) do
285+
variants = Map.fetch!(variants, target_triple)
286+
287+
for variant <- variants do
288+
tar_gz_file_url(
289+
base_url,
290+
lib_name_with_ext(target_triple, lib_name <> "--" <> Atom.to_string(variant))
291+
)
292+
end
293+
end
294+
295+
defp maybe_variants_tar_gz_urls(_, _, _, _), do: []
296+
256297
@doc """
257-
Returns the file URL to be downloaded for current target.
298+
Returns the file URLs to be downloaded for current target.
258299
300+
It is in the plural because a target may have some variants for it.
259301
It receives the NIF module.
260302
"""
261-
def current_target_nif_url(nif_module) when is_atom(nif_module) do
303+
def current_target_nif_urls(nif_module) when is_atom(nif_module) do
262304
metadata =
263305
nif_module
264306
|> metadata_file()
265307
|> read_map_from_file()
266308

267309
case metadata do
268310
%{base_url: base_url, file_name: file_name} ->
269-
tar_gz_file_url(base_url, file_name)
311+
target_triple = target_triple_from_nif_target(metadata[:target])
312+
313+
tar_gz_urls(base_url, file_name, target_triple, metadata[:variants])
270314

271315
_ ->
272316
raise "metadata about current target for the module #{inspect(nif_module)} is not available. " <>
273317
"Please compile the project again with: `mix compile --force`"
274318
end
275319
end
276320

321+
defp tar_gz_urls(base_url, file_name, target_triple, variants) do
322+
[lib_name, _] = String.split(file_name, ".", parts: 2)
323+
324+
[
325+
tar_gz_file_url(base_url, file_name)
326+
| maybe_variants_tar_gz_urls(variants, base_url, target_triple, lib_name)
327+
]
328+
end
329+
277330
@doc """
278331
Returns the target triple for download or compile and load.
279332
@@ -501,14 +554,19 @@ defmodule RustlerPrecompiled do
501554
crate: config.crate,
502555
otp_app: config.otp_app,
503556
targets: config.targets,
557+
variants: variants_for_metadata(config.variants),
504558
nif_versions: config.nif_versions,
505559
version: config.version
506560
}
507561

508562
case target(target_config(config.nif_versions), config.targets, config.nif_versions) do
509563
{:ok, target} ->
510564
basename = config.crate || config.otp_app
511-
lib_name = "#{lib_prefix(target)}#{basename}-v#{config.version}-#{target}"
565+
566+
target_triple = target_triple_from_nif_target(target)
567+
568+
variant = variant_suffix(target_triple, config)
569+
lib_name = "#{lib_prefix(target)}#{basename}-v#{config.version}-#{target}#{variant}"
512570

513571
file_name = lib_name_with_ext(target, lib_name)
514572

@@ -534,6 +592,38 @@ defmodule RustlerPrecompiled do
534592
end
535593
end
536594

595+
defp variants_for_metadata(variants) do
596+
Map.new(variants, fn {target, values} -> {target, Keyword.keys(values)} end)
597+
end
598+
599+
# Extract the target without the nif-NIF-VERSION part
600+
defp target_triple_from_nif_target(nif_target) do
601+
["nif", _version, triple] = String.split(nif_target, "-", parts: 3)
602+
triple
603+
end
604+
605+
defp variant_suffix(target, %{variants: variants} = config) when is_map_key(variants, target) do
606+
variants = Map.fetch!(variants, target)
607+
608+
callback = fn {_name, func} ->
609+
if is_function(func, 1) do
610+
func.(config)
611+
else
612+
func.()
613+
end
614+
end
615+
616+
case Enum.find(variants, callback) do
617+
{name, _} ->
618+
"--" <> Atom.to_string(name)
619+
620+
nil ->
621+
""
622+
end
623+
end
624+
625+
defp variant_suffix(_, _), do: ""
626+
537627
# Perform the download or load of the precompiled NIF
538628
# It will look in the "priv/native/otp_app" first, and if
539629
# that file doesn't exist, it will try to fetch from cache.

lib/rustler_precompiled/config.ex

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ defmodule RustlerPrecompiled.Config do
1313
:force_build?,
1414
:targets,
1515
:nif_versions,
16+
variants: %{},
1617
max_retries: 3
1718
]
1819

@@ -65,6 +66,7 @@ defmodule RustlerPrecompiled.Config do
6566
base_cache_dir: opts[:base_cache_dir],
6667
targets: targets,
6768
nif_versions: nif_versions,
69+
variants: validate_variants!(targets, Keyword.get(opts, :variants, %{})),
6870
max_retries: validate_max_retries!(Keyword.get(opts, :max_retries, 3))
6971
}
7072
end
@@ -125,4 +127,30 @@ defmodule RustlerPrecompiled.Config do
125127
end
126128

127129
defp pre_release?(version), do: "dev" in Version.parse!(version).pre
130+
131+
defp validate_variants!(_, nil), do: %{}
132+
133+
defp validate_variants!(targets, variants) when is_map(variants) do
134+
variants_targets = Map.keys(variants)
135+
136+
for target <- variants_targets do
137+
if target not in targets do
138+
raise "`:variants` contains a target that is not in the list of valid targets: #{inspect(target)}"
139+
end
140+
141+
possibilities = Map.fetch!(variants, target)
142+
143+
for {name, fun} <- possibilities do
144+
if not is_atom(name) do
145+
raise "`:variants` expects a keyword list as values, but found a key that is not an atom: #{inspect(name)}"
146+
end
147+
148+
if not (is_function(fun, 0) or is_function(fun, 1)) do
149+
raise "`:variants` expects a keyword list as values with functions to detect if a given variant is to be activated"
150+
end
151+
end
152+
end
153+
154+
variants
155+
end
128156
end

test/rustler_precompiled/config_test.exs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,4 +208,54 @@ defmodule RustlerPrecompiled.ConfigTest do
208208
opts = Keyword.update!(opts, :max_retries, fn _ -> nil end)
209209
assert_raise RuntimeError, fn -> Config.new(opts) end
210210
end
211+
212+
test "new/1 validates variants" do
213+
variants = %{"x86_64-unknown-linux-gnu" => [old_glibc: fn _config -> true end]}
214+
215+
opts = [
216+
otp_app: :rustler_precompiled,
217+
module: RustlerPrecompilationExample.Native,
218+
base_url:
219+
"https://github.com/philss/rustler_precompilation_example/releases/download/v0.2.0",
220+
variants: variants,
221+
version: "0.2.0-dev"
222+
]
223+
224+
assert Config.new(opts).variants == variants
225+
226+
zero_arity_variants = %{"x86_64-unknown-linux-gnu" => [old_glibc: fn -> true end]}
227+
opts = Keyword.update!(opts, :variants, fn _ -> zero_arity_variants end)
228+
229+
assert Config.new(opts).variants == zero_arity_variants
230+
231+
opts = Keyword.update!(opts, :variants, fn _ -> nil end)
232+
assert Config.new(opts).variants == %{}
233+
234+
invalid_target_in_variants = %{"x86_64-unknown-lizzard-hurd" => [old_glibc: fn -> true end]}
235+
opts = Keyword.update!(opts, :variants, fn _ -> invalid_target_in_variants end)
236+
237+
error_msg =
238+
~s|`:variants` contains a target that is not in the list of valid targets: "x86_64-unknown-lizzard-hurd"|
239+
240+
assert_raise RuntimeError, error_msg, fn -> Config.new(opts) end
241+
242+
more_than_one_arity_variants = %{
243+
"x86_64-unknown-linux-gnu" => [old_glibc: fn _config, _foo -> true end]
244+
}
245+
246+
opts = Keyword.update!(opts, :variants, fn _ -> more_than_one_arity_variants end)
247+
248+
error_msg =
249+
"`:variants` expects a keyword list as values with functions to detect if a given variant is to be activated"
250+
251+
assert_raise RuntimeError, error_msg, fn -> Config.new(opts) end
252+
253+
variants_without_keywords = %{"x86_64-unknown-linux-gnu" => [{"old_glibc", fn -> true end}]}
254+
opts = Keyword.update!(opts, :variants, fn _ -> variants_without_keywords end)
255+
256+
error_msg =
257+
~s|`:variants` expects a keyword list as values, but found a key that is not an atom: "old_glibc"|
258+
259+
assert_raise RuntimeError, error_msg, fn -> Config.new(opts) end
260+
end
211261
end

0 commit comments

Comments
 (0)