Skip to content

Commit ef75277

Browse files
authored
Fixes for rustler_mix (#682)
- Support Erlang-style NIF module names (:module_name) - Retrieve newest Rustler version without additional dependencies (fixes #680) - Update dependencies - Adjust .gitignore handling to match the new workspace style - Detect an existing workspace configuration and advise to add a newly generated project
1 parent e8876e9 commit ef75277

File tree

6 files changed

+145
-48
lines changed

6 files changed

+145
-48
lines changed

rustler_mix/lib/mix/tasks/rustler.new.ex

Lines changed: 121 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ defmodule Mix.Tasks.Rustler.New do
1616
@basic [
1717
{:eex, "basic/README.md", "README.md"},
1818
{:eex, "basic/Cargo.toml.eex", "Cargo.toml"},
19-
{:eex, "basic/src/lib.rs", "src/lib.rs"},
20-
{:text, "basic/.gitignore", ".gitignore"}
19+
{:eex, "basic/src/lib.rs", "src/lib.rs"}
2120
]
2221

2322
@root [
2423
{:eex, "root/Cargo.toml.eex", "Cargo.toml"}
2524
]
2625

26+
@fallback_version "0.36.1"
27+
2728
root = Path.join(:code.priv_dir(:rustler), "templates/")
2829

2930
for {format, source, _} <- @basic ++ @root do
@@ -50,6 +51,8 @@ defmodule Mix.Tasks.Rustler.New do
5051
module
5152
end
5253

54+
module_as_atom = parse_module_name!(module)
55+
5356
name =
5457
case opts[:name] do
5558
nil ->
@@ -69,41 +72,81 @@ defmodule Mix.Tasks.Rustler.New do
6972
otp_app -> otp_app
7073
end
7174

72-
check_module_name_validity!(module)
75+
rustler_version = rustler_version()
7376

7477
path = Path.join([File.cwd!(), "native", name])
75-
new(otp_app, path, module, name, opts)
76-
77-
copy_from(File.cwd!(), [library_name: name], @root)
78+
new(otp_app, path, module_as_atom, name, rustler_version, opts)
79+
80+
if Path.join(File.cwd!(), "Cargo.toml") |> File.exists?() do
81+
Mix.shell().info([
82+
:green,
83+
"Workspace Cargo.toml already exists, please add ",
84+
:bright,
85+
path |> Path.relative_to_cwd(),
86+
:reset,
87+
:green,
88+
" to the ",
89+
:bright,
90+
"\"members\"",
91+
:reset,
92+
:green,
93+
" list"
94+
])
95+
else
96+
copy_from(File.cwd!(), [library_name: name], @root)
97+
98+
gitignore = Path.join(File.cwd!(), ".gitignore")
99+
100+
if gitignore |> File.exists?() do
101+
Mix.shell().info([:green, "Updating .gitignore file"])
102+
File.write(gitignore, "\n# Rust binary artifacts\n/target/\n", [:append])
103+
else
104+
create_file(gitignore, "/target/\n")
105+
end
106+
end
78107

79-
Mix.Shell.IO.info([:green, "Ready to go! See #{path}/README.md for further instructions."])
108+
Mix.shell().info([
109+
:green,
110+
"\nReady to go! See #{path |> Path.relative_to_cwd()}/README.md for further instructions."
111+
])
80112
end
81113

82-
defp new(otp_app, path, module, name, _opts) do
83-
module_elixir = "Elixir." <> module
84-
114+
defp new(otp_app, path, module, name, rustler_version, _opts) do
85115
binding = [
86116
otp_app: otp_app,
87-
project_name: module_elixir,
88-
native_module: module_elixir,
89-
module: module,
117+
# Elixir syntax for the README
118+
module: module |> Macro.to_string(),
119+
# Erlang syntax for the init! invocation
120+
native_module: module |> Atom.to_string(),
90121
library_name: name,
91-
rustler_version: Rustler.rustler_version()
122+
rustler_version: rustler_version
92123
]
93124

94125
copy_from(path, binding, @basic)
95126
end
96127

97-
defp check_module_name_validity!(name) do
98-
if !(name =~ ~r/^[A-Z]\w*(\.[A-Z]\w*)*$/) do
99-
Mix.raise(
100-
"Module name must be a valid Elixir alias (for example: Foo.Bar), got: #{inspect(name)}"
101-
)
128+
defp parse_module_name!(name) do
129+
case Code.string_to_quoted(name) do
130+
{:ok, atom} when is_atom(atom) ->
131+
atom
132+
133+
{:ok, {:__aliases__, _, parts}} ->
134+
Module.concat(parts)
135+
136+
_ ->
137+
Mix.raise(
138+
"Module name must be a valid Elixir alias (for example: Foo.Bar, or :foo_bar), got: #{inspect(name)}"
139+
)
102140
end
103141
end
104142

105143
defp format_module_name_as_name(module_name) do
106-
String.replace(String.downcase(module_name), ".", "_")
144+
if module_name |> String.starts_with?(":") do
145+
# Skip first
146+
module_name |> String.downcase() |> String.slice(1..-1//1)
147+
else
148+
module_name |> String.downcase() |> String.replace(".", "_")
149+
end
107150
end
108151

109152
defp copy_from(target_dir, binding, mapping) when is_list(mapping) do
@@ -134,9 +177,66 @@ defmodule Mix.Tasks.Rustler.New do
134177
end
135178

136179
defp prompt(message) do
137-
Mix.Shell.IO.print_app()
180+
Mix.shell().print_app()
138181
resp = IO.gets(IO.ANSI.format([message, :white, " > "]))
139182
?\n = :binary.last(resp)
140183
:binary.part(resp, {0, byte_size(resp) - 1})
141184
end
185+
186+
@doc false
187+
defp rustler_version do
188+
versions =
189+
case Mix.Utils.read_path("https://crates.io/api/v1/crates/rustler",
190+
timeout: 10_000,
191+
unsafe_uri: true
192+
) do
193+
{:ok, body} ->
194+
get_versions(body)
195+
196+
err ->
197+
raise err
198+
end
199+
200+
try do
201+
result =
202+
versions
203+
|> Enum.map(&Version.parse!/1)
204+
|> Enum.filter(&(&1.pre == []))
205+
|> Enum.max(Version)
206+
|> Version.to_string()
207+
208+
Mix.shell().info("Fetched latest rustler crate version: #{result}")
209+
result
210+
rescue
211+
ex ->
212+
Mix.shell().error(
213+
"Failed to fetch rustler crate versions, using hardcoded fallback: #{@fallback_version}\nError: #{ex |> Kernel.inspect()}"
214+
)
215+
216+
@fallback_version
217+
end
218+
end
219+
220+
defp get_versions(data) do
221+
cond do
222+
# Erlang 27
223+
Code.ensure_loaded?(:json) and Kernel.function_exported?(:json, :decode, 1) ->
224+
data |> :json.decode() |> versions_from_parsed_json()
225+
226+
Code.ensure_loaded?(Jason) ->
227+
data |> Jason.decode!() |> versions_from_parsed_json()
228+
229+
true ->
230+
# Nasty hack: Instead of parsing the JSON, we use a regex, abusing the
231+
# compact nature of the returned data
232+
Regex.scan(~r/"num":"([^"]+)"/, data) |> Enum.map(fn [_, res] -> res end)
233+
end
234+
end
235+
236+
defp versions_from_parsed_json(parsed) do
237+
parsed
238+
|> Map.fetch!("versions")
239+
|> Enum.filter(fn version -> not version["yanked"] end)
240+
|> Enum.map(fn version -> version["num"] end)
241+
end
142242
end

rustler_mix/lib/rustler.ex

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -169,16 +169,4 @@ defmodule Rustler do
169169
end
170170
end
171171
end
172-
173-
@doc false
174-
def rustler_version do
175-
# Retrieve newest version or fall back to hard-coded one
176-
Req.get!("https://crates.io/api/v1/crates/rustler").body
177-
|> Map.fetch!("versions")
178-
|> Enum.filter(fn version -> not version["yanked"] end)
179-
|> Enum.map(fn version -> version["num"] end)
180-
|> Enum.fetch!(0)
181-
rescue
182-
_ -> "0.34.0"
183-
end
184172
end

rustler_mix/mix.exs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,9 @@ defmodule Rustler.Mixfile do
2424

2525
defp deps do
2626
[
27-
{:toml, "~> 0.6", runtime: false},
27+
{:toml, "~> 0.7", runtime: false},
2828
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
29-
{:jason, "~> 1.0", runtime: false},
30-
{:req, "~> 0.5", runtime: false}
29+
{:jason, "~> 1.0", runtime: false}
3130
]
3231
end
3332

rustler_mix/priv/templates/basic/.gitignore

Lines changed: 0 additions & 1 deletion
This file was deleted.

rustler_mix/priv/templates/basic/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# NIF for <%= project_name %>
1+
# NIF for <%= module %>
22

33
## To build the NIF module:
44

rustler_mix/test.sh

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88

99
set -e
1010

11-
rustler_mix=$PWD
12-
rustler=$(realpath $PWD/../rustler)
11+
rustler_mix=$(realpath $(dirname $0))
12+
rustler=$(realpath $rustler_mix/../rustler)
1313
tmp=$(mktemp --directory)
1414

15+
export MIX_ARCHIVES="$tmp/mix_archives/"
16+
1517
#
1618
# Test Steps
1719
#
@@ -24,12 +26,21 @@ tmp=$(mktemp --directory)
2426
# * Check that the NIF can be loaded and used
2527
#
2628

29+
echo "Build and install archive"
30+
echo
31+
mix local.hex --force
32+
MIX_ENV=prod mix archive.build -o "$tmp/rustler.ez"
33+
mix archive.install --force "$tmp/rustler.ez"
34+
35+
echo
2736
echo "Creating a new mix project and rustler template in $tmp"
37+
echo
2838
cd $tmp
2939

40+
mkdir archives
41+
3042
mix new test_rustler_mix
3143
cd test_rustler_mix
32-
mkdir -p priv/native
3344

3445
cat >mix.exs <<EOF
3546
defmodule TestRustlerMix.MixProject do
@@ -51,14 +62,14 @@ defmodule TestRustlerMix.MixProject do
5162
end
5263
EOF
5364

54-
mix deps.get
55-
mix deps.compile
65+
mix rustler.new --module RustlerMixTest --name rustler_mix_test || exit 1
5666

57-
mix rustler.new --module RustlerMixTest --name rustler_mix_test
67+
mix deps.get || exit 1
68+
mix deps.compile || exit 1
5869

5970
sed -i "s|^rustler.*$|rustler = { path = \"$rustler\" }|" native/rustler_mix_test/Cargo.toml
6071

61-
mix compile
72+
mix compile || exit 1
6273

6374
# Delete everything except the templated module from the generated README
6475

@@ -88,12 +99,12 @@ defmodule RustlerMixTestTest do
8899
end
89100
EOF
90101

91-
mix test
102+
mix test || exit 1
92103

93104
# See https://github.com/rusterlium/rustler/issues/516, we also need to verify that everything
94105
# we need is part of a release.
95-
mix release
96-
_build/dev/rel/test_rustler_mix/bin/test_rustler_mix eval 'RustlerMixTest.add(1, 2)'
106+
mix release || exit 1
107+
_build/dev/rel/test_rustler_mix/bin/test_rustler_mix eval 'RustlerMixTest.add(1, 2)' || exit 1
97108

98109
echo "Done; cleaning up"
99110
rm -r $tmp

0 commit comments

Comments
 (0)