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