defmodule Rx.MixProject do
use Mix.Project
@source_url "https://github.com/pklonowski/rx"
@version "0.1.0"
def project do
[
app: :rx,
version: @version,
elixir: "~> 1.18",
description: description(),
source_url: @source_url,
homepage_url: @source_url,
start_permanent: Mix.env() == :prod,
compilers: compilers(),
deps: deps(),
docs: docs(),
package: package()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
mod: {Rx.Application, []},
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:jason, "~> 1.4"},
{:explorer, "~> 0.11", optional: true},
{:plotly_ex, "~> 0.1", optional: true},
{:kino, "~> 0.19.0", optional: true},
{:ex_doc, "~> 0.40.3", only: :dev, runtime: false}
]
end
defp description do
"Drive R from Elixir through a persistent Rscript backend, with an experimental embedded native backend."
end
defp docs do
[
main: "readme",
extras: ["README.md"],
source_ref: "v#{@version}",
source_url: @source_url
]
end
defp package do
[
licenses: ["MIT"],
links: %{
"GitHub" => @source_url
},
files: [
".formatter.exs",
"LICENSE",
"Makefile",
"README.md",
"native/c_src",
"lib",
"mix.exs",
"native/rx_rust_nif/Cargo.toml",
"native/rx_rust_nif/Cargo.lock",
"native/rx_rust_nif/src",
"notebooks/iris_classification_r_guide.livemd",
"notebooks/port_arrow_native_benchmark.livemd",
"notebooks/renv_process_backend_smoke.livemd",
"notebooks/rx_tour.livemd",
"priv/rx_backend.R"
]
]
end
defp compilers do
case native_gate() do
{:ok, :c} -> Mix.compilers() ++ [:rx_nif]
{:ok, :rust} -> Mix.compilers() ++ [:rx_rust_nif]
:disabled -> Mix.compilers()
{:error, message} -> Mix.raise(message)
end
end
defp native_gate do
case {native_gate_enabled?("RX_BUILD_NIF"), native_gate_enabled?("RX_BUILD_RUST_NIF")} do
{true, false} -> {:ok, :c}
{false, true} -> {:ok, :rust}
{false, false} -> :disabled
{true, true} -> {:error, native_gate_mutual_exclusion_message()}
end
end
defp native_gate_enabled?(name), do: System.get_env(name) == "1"
defp native_gate_mutual_exclusion_message do
"RX_BUILD_NIF=1 and RX_BUILD_RUST_NIF=1 cannot both be set; set exactly one native implementation gate. " <>
"The C and Rust native implementations " <>
"both load as priv/rx_nif.so and cannot be active in the same BEAM"
end
end
defmodule Mix.Tasks.Compile.RxNif do
@moduledoc false
use Mix.Task.Compiler
@impl true
def run(_args) do
app_path = Mix.Project.app_path()
erts_include_dir =
:code.root_dir() |> Path.join("erts-#{:erlang.system_info(:version)}/include")
{output, status} =
System.cmd("make", ["-B"],
env: [
{"MIX_APP_PATH", app_path},
{"ERTS_INCLUDE_DIR", erts_include_dir}
],
stderr_to_stdout: true
)
IO.write(output)
if status == 0 do
{:ok, []}
else
{:error, []}
end
end
end
defmodule Mix.Tasks.Compile.RxRustNif do
@moduledoc false
use Mix.Task.Compiler
@crate_dir Path.join(["native", "rx_rust_nif"])
@rust_install_instructions """
RX_BUILD_RUST_NIF=1 requested the Rust native NIF, but cargo is missing or unusable.
Install Rust with rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
. "$HOME/.cargo/env"
rustc --version
cargo --version
On Debian/Ubuntu-style systems, also install native build tools:
sudo apt-get update
sudo apt-get install -y build-essential pkg-config
"""
@impl true
def run(_args) do
cargo = System.get_env("RX_RUST_NIF_CARGO") || "cargo"
with :ok <- ensure_cargo!(cargo),
:ok <- build_crate(cargo),
:ok <- copy_artifact() do
{:ok, []}
else
{:error, message} ->
Mix.shell().error(message)
{:error, []}
end
end
defp ensure_cargo!(cargo) do
case System.cmd(cargo, ["--version"], stderr_to_stdout: true) do
{output, 0} ->
IO.puts(String.trim(output))
:ok
{_output, _status} ->
{:error, @rust_install_instructions}
end
rescue
ErlangError -> {:error, @rust_install_instructions}
end
defp build_crate(cargo) do
{output, status} =
System.cmd(cargo, ["build", "--release"],
cd: @crate_dir,
stderr_to_stdout: true
)
IO.write(output)
if status == 0 do
:ok
else
{:error, "cargo build --release failed for #{@crate_dir}"}
end
end
defp copy_artifact do
app_path = Mix.Project.app_path()
priv_dir = Path.join(app_path, "priv")
File.mkdir_p!(priv_dir)
source = rust_artifact_path()
destination = Path.join(priv_dir, "rx_nif.so")
File.rm(destination)
File.cp!(source, destination)
IO.puts("Copied Rust NIF artifact from #{source} to #{destination}")
:ok
end
defp rust_artifact_path do
extension =
case :os.type() do
{:unix, :darwin} -> "dylib"
{:win32, _name} -> "dll"
_other -> "so"
end
Path.join([@crate_dir, "target", "release", "librx_rust_nif.#{extension}"])
end
end