Skip to content
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

Fixes for rustler_mix #682

Merged
merged 1 commit into from
Feb 1, 2025
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
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
Loading