Skip to content

Commit

Permalink
Fixes for rustler_mix (#682)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
filmor authored Feb 1, 2025
1 parent e8876e9 commit ef75277
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 48 deletions.
142 changes: 121 additions & 21 deletions rustler_mix/lib/mix/tasks/rustler.new.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ defmodule Mix.Tasks.Rustler.New do
@basic [
{:eex, "basic/README.md", "README.md"},
{:eex, "basic/Cargo.toml.eex", "Cargo.toml"},
{:eex, "basic/src/lib.rs", "src/lib.rs"},
{:text, "basic/.gitignore", ".gitignore"}
{:eex, "basic/src/lib.rs", "src/lib.rs"}
]

@root [
{:eex, "root/Cargo.toml.eex", "Cargo.toml"}
]

@fallback_version "0.36.1"

root = Path.join(:code.priv_dir(:rustler), "templates/")

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

module_as_atom = parse_module_name!(module)

name =
case opts[:name] do
nil ->
Expand All @@ -69,41 +72,81 @@ defmodule Mix.Tasks.Rustler.New do
otp_app -> otp_app
end

check_module_name_validity!(module)
rustler_version = rustler_version()

path = Path.join([File.cwd!(), "native", name])
new(otp_app, path, module, name, opts)

copy_from(File.cwd!(), [library_name: name], @root)
new(otp_app, path, module_as_atom, name, rustler_version, opts)

if Path.join(File.cwd!(), "Cargo.toml") |> File.exists?() do
Mix.shell().info([
:green,
"Workspace Cargo.toml already exists, please add ",
:bright,
path |> Path.relative_to_cwd(),
:reset,
:green,
" to the ",
:bright,
"\"members\"",
:reset,
:green,
" list"
])
else
copy_from(File.cwd!(), [library_name: name], @root)

gitignore = Path.join(File.cwd!(), ".gitignore")

if gitignore |> File.exists?() do
Mix.shell().info([:green, "Updating .gitignore file"])
File.write(gitignore, "\n# Rust binary artifacts\n/target/\n", [:append])
else
create_file(gitignore, "/target/\n")
end
end

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

defp new(otp_app, path, module, name, _opts) do
module_elixir = "Elixir." <> module

defp new(otp_app, path, module, name, rustler_version, _opts) do
binding = [
otp_app: otp_app,
project_name: module_elixir,
native_module: module_elixir,
module: module,
# Elixir syntax for the README
module: module |> Macro.to_string(),
# Erlang syntax for the init! invocation
native_module: module |> Atom.to_string(),
library_name: name,
rustler_version: Rustler.rustler_version()
rustler_version: rustler_version
]

copy_from(path, binding, @basic)
end

defp check_module_name_validity!(name) do
if !(name =~ ~r/^[A-Z]\w*(\.[A-Z]\w*)*$/) do
Mix.raise(
"Module name must be a valid Elixir alias (for example: Foo.Bar), got: #{inspect(name)}"
)
defp parse_module_name!(name) do
case Code.string_to_quoted(name) do
{:ok, atom} when is_atom(atom) ->
atom

{:ok, {:__aliases__, _, parts}} ->
Module.concat(parts)

_ ->
Mix.raise(
"Module name must be a valid Elixir alias (for example: Foo.Bar, or :foo_bar), got: #{inspect(name)}"
)
end
end

defp format_module_name_as_name(module_name) do
String.replace(String.downcase(module_name), ".", "_")
if module_name |> String.starts_with?(":") do
# Skip first
module_name |> String.downcase() |> String.slice(1..-1//1)
else
module_name |> String.downcase() |> String.replace(".", "_")
end
end

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

defp prompt(message) do
Mix.Shell.IO.print_app()
Mix.shell().print_app()
resp = IO.gets(IO.ANSI.format([message, :white, " > "]))
?\n = :binary.last(resp)
:binary.part(resp, {0, byte_size(resp) - 1})
end

@doc false
defp rustler_version do
versions =
case Mix.Utils.read_path("https://crates.io/api/v1/crates/rustler",
timeout: 10_000,
unsafe_uri: true
) do
{:ok, body} ->
get_versions(body)

err ->
raise err
end

try do
result =
versions
|> Enum.map(&Version.parse!/1)
|> Enum.filter(&(&1.pre == []))
|> Enum.max(Version)
|> Version.to_string()

Mix.shell().info("Fetched latest rustler crate version: #{result}")
result
rescue
ex ->
Mix.shell().error(
"Failed to fetch rustler crate versions, using hardcoded fallback: #{@fallback_version}\nError: #{ex |> Kernel.inspect()}"
)

@fallback_version
end
end

defp get_versions(data) do
cond do
# Erlang 27
Code.ensure_loaded?(:json) and Kernel.function_exported?(:json, :decode, 1) ->
data |> :json.decode() |> versions_from_parsed_json()

Code.ensure_loaded?(Jason) ->
data |> Jason.decode!() |> versions_from_parsed_json()

true ->
# Nasty hack: Instead of parsing the JSON, we use a regex, abusing the
# compact nature of the returned data
Regex.scan(~r/"num":"([^"]+)"/, data) |> Enum.map(fn [_, res] -> res end)
end
end

defp versions_from_parsed_json(parsed) do
parsed
|> Map.fetch!("versions")
|> Enum.filter(fn version -> not version["yanked"] end)
|> Enum.map(fn version -> version["num"] end)
end
end
12 changes: 0 additions & 12 deletions rustler_mix/lib/rustler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -169,16 +169,4 @@ defmodule Rustler do
end
end
end

@doc false
def rustler_version do
# Retrieve newest version or fall back to hard-coded one
Req.get!("https://crates.io/api/v1/crates/rustler").body
|> Map.fetch!("versions")
|> Enum.filter(fn version -> not version["yanked"] end)
|> Enum.map(fn version -> version["num"] end)
|> Enum.fetch!(0)
rescue
_ -> "0.34.0"
end
end
5 changes: 2 additions & 3 deletions rustler_mix/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@ defmodule Rustler.Mixfile do

defp deps do
[
{:toml, "~> 0.6", runtime: false},
{:toml, "~> 0.7", runtime: false},
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
{:jason, "~> 1.0", runtime: false},
{:req, "~> 0.5", runtime: false}
{:jason, "~> 1.0", runtime: false}
]
end

Expand Down
1 change: 0 additions & 1 deletion rustler_mix/priv/templates/basic/.gitignore

This file was deleted.

2 changes: 1 addition & 1 deletion rustler_mix/priv/templates/basic/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# NIF for <%= project_name %>
# NIF for <%= module %>

## To build the NIF module:

Expand Down
31 changes: 21 additions & 10 deletions rustler_mix/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@

set -e

rustler_mix=$PWD
rustler=$(realpath $PWD/../rustler)
rustler_mix=$(realpath $(dirname $0))
rustler=$(realpath $rustler_mix/../rustler)
tmp=$(mktemp --directory)

export MIX_ARCHIVES="$tmp/mix_archives/"

#
# Test Steps
#
Expand All @@ -24,12 +26,21 @@ tmp=$(mktemp --directory)
# * Check that the NIF can be loaded and used
#

echo "Build and install archive"
echo
mix local.hex --force
MIX_ENV=prod mix archive.build -o "$tmp/rustler.ez"
mix archive.install --force "$tmp/rustler.ez"

echo
echo "Creating a new mix project and rustler template in $tmp"
echo
cd $tmp

mkdir archives

mix new test_rustler_mix
cd test_rustler_mix
mkdir -p priv/native

cat >mix.exs <<EOF
defmodule TestRustlerMix.MixProject do
Expand All @@ -51,14 +62,14 @@ defmodule TestRustlerMix.MixProject do
end
EOF

mix deps.get
mix deps.compile
mix rustler.new --module RustlerMixTest --name rustler_mix_test || exit 1

mix rustler.new --module RustlerMixTest --name rustler_mix_test
mix deps.get || exit 1
mix deps.compile || exit 1

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

mix compile
mix compile || exit 1

# Delete everything except the templated module from the generated README

Expand Down Expand Up @@ -88,12 +99,12 @@ defmodule RustlerMixTestTest do
end
EOF

mix test
mix test || exit 1

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

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

0 comments on commit ef75277

Please sign in to comment.