Skip to content

Commit 66ed1eb

Browse files
committed
mix nif compiler prototype
1 parent 0b8fdb5 commit 66ed1eb

15 files changed

+651
-0
lines changed

rustler_mix/.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/_build
2+
/cover
3+
/deps
4+
erl_crash.dump
5+
*.ez
6+
7+
/src/toml_parser.erl
8+
/src/toml_lexer.erl

rustler_mix/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Rustler
2+
3+
**TODO: Add description**
4+
5+
## Installation
6+
7+
If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:
8+
9+
1. Add rustler_mix to your list of dependencies in `mix.exs`:
10+
11+
def deps do
12+
[{:rustler_mix, "~> 0.0.1"}]
13+
end
14+
15+
2. Ensure rustler_mix is started before your application:
16+
17+
def application do
18+
[applications: [:rustler_mix]]
19+
end
20+

rustler_mix/config/config.exs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# This file is responsible for configuring your application
2+
# and its dependencies with the aid of the Mix.Config module.
3+
use Mix.Config
4+
5+
# This configuration is loaded before any dependency and is restricted
6+
# to this project. If another project depends on this project, this
7+
# file won't be loaded nor affect the parent project. For this reason,
8+
# if you want to provide default values for your application for
9+
# 3rd-party users, it should be done in your "mix.exs" file.
10+
11+
# You can configure for your application as:
12+
#
13+
# config :rustler_mix, key: :value
14+
#
15+
# And access this configuration in your application as:
16+
#
17+
# Application.get_env(:rustler_mix, :key)
18+
#
19+
# Or configure a 3rd-party app:
20+
#
21+
# config :logger, level: :info
22+
#
23+
24+
# It is also possible to import configuration files, relative to this
25+
# directory. For example, you can emulate configuration per environment
26+
# by uncommenting the line below and defining dev.exs, test.exs and such.
27+
# Configuration from the imported file will override the ones defined
28+
# here (which is why it is important to import them last).
29+
#
30+
# import_config "#{Mix.env}.exs"

rustler_mix/lib/compiler.ex

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
defmodule Mix.Tasks.Compile.Rustler do
2+
use Mix.Task
3+
4+
alias Rustler.Compiler.Messages
5+
alias Rustler.Compiler.Multirust
6+
7+
def throw_error(error_descr) do
8+
Mix.shell.error Messages.message(error_descr)
9+
raise "Compilation error"
10+
end
11+
12+
def run(_args) do
13+
project = Mix.Project.config
14+
crates = project[:rustler_crates]
15+
16+
if Multirust.version == :none do
17+
throw_error(:multirust_not_installed)
18+
end
19+
20+
rust_versions = Multirust.rust_versions
21+
unless Rustler.rust_version in rust_versions do
22+
throw_error({:rust_version_not_installed, Rustler.rust_version})
23+
end
24+
25+
nif_version = :erlang.system_info(:nif_version)
26+
unless nif_version in Rustler.nif_versions do
27+
throw_error({:unsupported_nif_version, nif_version})
28+
end
29+
30+
File.mkdir_p!(Rustler.nif_lib_dir)
31+
32+
is_release = System.get_env("RUSTLER_DEBUG") != "true"
33+
compile_crates(crates, is_release)
34+
end
35+
36+
def compile_crates(nil, _), do: throw_error(:no_crates_property)
37+
def compile_crates(crates, release) do
38+
Enum.map(crates, &compile_crate(&1, release))
39+
end
40+
41+
def compile_crate(crate, release) do
42+
cargo_data = check_crate_env(crate)
43+
44+
lib_name = Rustler.TomlParser.get_table_val(cargo_data, ["lib"], "name")
45+
if lib_name == nil do
46+
throw_error({:cargo_no_library, crate})
47+
end
48+
49+
compiler_flags = if release, do: ["--release"], else: []
50+
compile_flavor = if release, do: "release", else: "debug"
51+
52+
target_dir = "#{Rustler.nif_lib_dir}/#{crate}_target"
53+
54+
Mix.shell.info "Compiling NIF crate '#{crate}' (#{compile_flavor})..."
55+
case Multirust.compile_with(crate, Rustler.rust_version, compiler_flags,
56+
[{"CARGO_TARGET_DIR", target_dir}],
57+
IO.stream(:stdio, :line)) do
58+
{_, 0} -> nil
59+
_ -> raise "Compile error"
60+
end
61+
62+
# TODO: Make build flavor specific
63+
Enum.map(Path.wildcard("#{target_dir}/#{compile_flavor}/lib#{lib_name}.*"), fn
64+
path ->
65+
file_name = Path.basename(path)
66+
File.cp!(Path.absname(path), "#{Rustler.nif_lib_dir}/#{file_name}")
67+
end)
68+
end
69+
70+
def check_crate_env(crate) do
71+
unless File.dir?(crate) do
72+
Mix.shell.error Messages.message(:no_crates_property_message)
73+
end
74+
75+
cargo_data =
76+
case File.read("#{crate}/Cargo.toml") do
77+
{:error, :enoent} ->
78+
throw_error({:cargo_toml_not_found, crate})
79+
{:ok, text} ->
80+
Rustler.TomlParser.parse(text)
81+
end
82+
83+
rustler_version = get_cargo_dep_info(cargo_data, "rustler")
84+
rustler_codegen_version = get_cargo_dep_info(cargo_data, "rustler_codegen")
85+
86+
validate_versions(crate, rustler_version, rustler_codegen_version)
87+
88+
cargo_data
89+
end
90+
91+
def validate_versions(crate, rustler, codegen)
92+
when rustler == nil or codegen == nil do
93+
throw_error({:no_rustler_deps, crate, Rustler.rustler_version})
94+
end
95+
def validate_versions(_, %{version: nil}, %{version: nil}) do
96+
# Development, no validation
97+
end
98+
def validate_versions(crate, %{version: rustler_v}, %{version: codegen_v})
99+
when rustler_v != codegen_v do
100+
throw_error({:differing_versions, crate, rustler_v, codegen_v})
101+
end
102+
def validate_versions(crate, %{version: version}, %{version: version}) do
103+
unless version == "=#{Rustler.rustler_version}" do
104+
throw_error({:unsupported_rustler_version, crate,
105+
"=#{Rustler.rustler_version}", version})
106+
end
107+
end
108+
109+
def get_cargo_dep_info(data, dep_name) do
110+
table_path = ["dependencies", dep_name]
111+
simple_key = Rustler.TomlParser.get_table_val(data, ["dependencies"], dep_name)
112+
table = Rustler.TomlParser.get_table_vals(data, ["dependencies", dep_name])
113+
114+
cond do
115+
simple_key != nil ->
116+
%{version: simple_key}
117+
table != nil ->
118+
%{
119+
version: Rustler.TomlParser.get_keys_key(table, "version"),
120+
path: Rustler.TomlParser.get_keys_key(table, "path"),
121+
git: Rustler.TomlParser.get_keys_key(table, "git"),
122+
}
123+
true ->
124+
nil
125+
end
126+
end
127+
128+
end

rustler_mix/lib/compiler/messages.ex

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
defmodule Rustler.Compiler.Messages do
2+
3+
def message(:multirust_not_installed) do
4+
"""
5+
Multirust could not be found on your machine.
6+
It is required for building NIF crates automatically.
7+
8+
Follow the instructions in https://github.com/brson/multirust to install it.
9+
"""
10+
end
11+
12+
def message({:nonexistent_crate_directory, crate}) do
13+
"""
14+
Directory '#{crate}' could not be found.
15+
One of the crate directories specified for compilation does not exist.
16+
"""
17+
end
18+
19+
def message({:cargo_toml_not_found, crate}) do
20+
"""
21+
No 'Cargo.toml' could be found in the '#{crate}' directory.
22+
Are you sure it is a Rust crate?
23+
"""
24+
end
25+
26+
def message(:no_crates_property) do
27+
"""
28+
No NIF crates listed in project definition.
29+
Add some crate directories under :rustler_crates in the mix.exs project definition to compile them.
30+
31+
Example:
32+
rustler_crates: ["my_nif_crate/"]
33+
"""
34+
end
35+
36+
def message({:differing_versions, crate, rustler_version, codegen_version}) do
37+
"""
38+
The '#{crate}' crate should have the same rustler and rustler_codegen version in its Cargo.toml:
39+
rustler version: #{rustler_version}
40+
rustler_codegen version: #{codegen_version}
41+
"""
42+
end
43+
44+
def message({:unsupported_rustler_version, crate, supported, version}) do
45+
"""
46+
The crate '#{crate}' is using an unsupported version of rustler and rustler_codegen.
47+
48+
Should be: '#{supported}''
49+
Is: '#{version}'
50+
51+
Note: The version numbers need to match exactly, including space and equals.
52+
"""
53+
end
54+
55+
def message({:rust_version_not_installed, needed_version}) do
56+
"""
57+
Required Rust toolchain version #{needed_version} is not installed.
58+
It can be installed by running 'multirust update #{needed_version}'.
59+
"""
60+
end
61+
62+
def message({:no_rustler_deps, crate, version}) do
63+
"""
64+
No Rustler dependencies found in Cargo.toml for crate #{crate}.
65+
Please make sure the following is in the dependencies section of your Cargo.toml file:
66+
67+
> rustler = #{version}
68+
> rustler_codegen = #{version}
69+
70+
Note: You should already have this if you made your project with the project generator.
71+
"""
72+
end
73+
74+
def message({:cargo_no_library, crate}) do
75+
"""
76+
No library with name listed in Cargo.toml of crate '#{crate}'.
77+
"""
78+
end
79+
80+
end

rustler_mix/lib/compiler/multirust.ex

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
defmodule Rustler.Compiler.Multirust do
2+
3+
def version do
4+
multirust_exec = System.find_executable("multirust")
5+
if multirust_exec == nil do
6+
:none
7+
else
8+
{version, 0} = System.cmd("multirust", ["--version"])
9+
{:ok, version}
10+
end
11+
end
12+
13+
def rust_versions do
14+
{versions_raw, 0} = System.cmd("multirust", ["list-toolchains"])
15+
versions_raw
16+
|> String.strip(?\n)
17+
|> String.split("\n")
18+
end
19+
20+
def install_toolchain(version) do
21+
{_resp, 0} = System.cmd("multirust", ["update", version],
22+
into: IO.stream(:stdio, :line))
23+
end
24+
25+
def compile_with(path, version, args \\ [], env \\ [], into \\ "") do
26+
System.cmd("multirust", ["run", version, "cargo", "build" | args],
27+
cd: path, into: into, env: env)
28+
end
29+
30+
end

rustler_mix/lib/rustler.ex

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
defmodule Rustler do
2+
3+
def rustler_version, do: "0.8.1"
4+
def rust_version, do: "nightly-2016-04-05"
5+
def nif_versions, do: [
6+
'2.7',
7+
'2.8',
8+
'2.9',
9+
'2.10',
10+
]
11+
12+
def nif_lib_dir do
13+
Mix.Project.build_path <> "/rustler"
14+
end
15+
16+
def nif_lib_path(lib_name) do
17+
"#{nif_lib_dir}/lib#{lib_name}"
18+
end
19+
20+
defmacro load_nif(lib_name, load_args \\ nil) do
21+
quote do
22+
:erlang.load_nif(Rustler.nif_lib_path(unquote(lib_name)), unquote(load_args))
23+
end
24+
end
25+
26+
end

0 commit comments

Comments
 (0)