require Logger defmodule ElixirStan do @moduledoc """ Documentation for `ElixirStan`. """ @doc """ Get the version of the stanc3 executable bundled with the library. """ @spec stanc_version :: String.t() | {:error, String.t()} def stanc_version() do case System.cmd(Application.app_dir(:elixir_stan, "/priv/bin/stanc"), ["--version"]) do {version_string, 0} -> String.trim(version_string) {error, _} -> {:error, error} end end @doc """ Computes the hash of a file used for caching. Computes SHA256 of the file and truncates it to 8 bytes, then encodes it in a hex string. """ @spec hash_file(binary()) :: String.t() def hash_file(file) when is_binary(file) do <> = :crypto.hash(:sha256, file) Base.encode16 hash end @spec model_tmp_dir(Path.t()) :: binary | {:error, atom} def model_tmp_dir(path) do with {:ok, model_code} <- File.read(path) do model_name = Path.basename(path, ".stan") model_hash = hash_file(model_code) Path.join([ System.tmp_dir(), "elixir_stan", model_name <> model_hash <> String.replace(stanc_version(), " ", "_") ]) end end @doc """ Transpiles the model into C++ and stores it in a temporary directory. Temporary directory is based on `System.tmp_dir()`. An `elixir_stan` directory is created in the system temporary directory and the model is stored in a subdirectory of that. The model directory is based on the model name, the hash of the model code and the version of stanc used to compile it. """ @spec transpile_model(Path.t()) :: :ok | {:error, any()} def transpile_model(path) do with {:ok, model_code} <- File.read(path) do model_tmp_path = model_tmp_dir(path) # Create model dir :ok = File.mkdir_p(model_tmp_path) model_tmp_file = Path.join(model_tmp_path, Path.basename(path)) # Write model code :ok = File.write(model_tmp_file, model_code) case System.cmd(Application.app_dir(:elixir_stan, "/priv/bin/stanc"), [model_tmp_file]) do {_, 0} -> :ok end end end @doc """ Cleans the model tmp directory. """ @spec clean_model(Path.t()) :: :ok | {:error, String.t()} def clean_model(path) do model_tmp_path = model_tmp_dir(path) # Remove model dir case File.rm_rf(model_tmp_path) do {:ok, _} -> :ok {:error, error, file} -> Logger.error( "Could not clean model: #{path}\n\t Error: #{:file.format_error(error)}\n\t File: #{file}" ) {:error, :file.format_error(error)} end end end