defmodule SqliteVss do @moduledoc """ `SqliteVss` module which installs the vector0 and vss0 extensions. """ use Application require Logger @doc false def start(_, _) do Supervisor.start_link([], strategy: :one_for_one) end @doc """ Returns the configured sqlite_vss version. """ def current_version do config = :hex_core.default_config() {:ok, _} = Application.ensure_all_started(:inets) {:ok, _} = Application.ensure_all_started(:ssl) updated_config = Map.put(config, :http_adapter, {:hex_http_httpc, %{http_options: http_options()}}) case :hex_api_package.get(updated_config, "sqlite_vss") do {:ok, {200, _headers, response}} -> response |> Map.get("releases") |> Enum.map(& &1["version"]) |> List.first() {:ok, {404, _, _}} -> # TODO: remove this fallback once sqlite_vss is published "0.1.1-alpha.8" {:error, reason} -> reason end end @doc """ Installs sqlite_vss with `current_version/0`. """ def install(base_url \\ default_base_url()) do url = get_url(base_url) tar_binary = fetch_body!(url) bin_path = bin_path() with :ok <- :erl_tar.extract({:binary, tar_binary}, [ :compressed, cwd: to_charlist(bin_path) ]) do Logger.debug("Copying artifact from release and extracting to #{bin_path}") :ok end end @doc """ Returns the path to the executable. The executable may not be available if it was not yet installed. """ def bin_path do {:ok, current_target} = current_target() name = "sqlite-vss-#{current_target}" Application.get_env(:sqlite_vss, :path) || if Code.ensure_loaded?(Mix.Project) do Path.join(Path.dirname(Mix.Project.build_path()), name) else Path.expand("_build/#{name}") end end def filename(target) do "sqlite-vss-v#{current_version()}-loadable-#{target}.tar.gz" end @doc """ The default URL to install SqliteVss from. """ def default_base_url do "https://github.com/asg017/sqlite-vss/releases/download/v$version/sqlite-vss-v$version-loadable-$target.tar.gz" end def loadable_path_vector0() do SqliteVss.bin_path() <> "/vector0" end def loadable_path_vss0() do SqliteVss.bin_path() <> "/vss0" end defp target_config do current_system_arch = system_arch() %{ os_type: :os.type(), target_system: current_system_arch, word_size: :erlang.system_info(:wordsize) } end def current_target!(target_config \\ target_config()) do current_target(target_config) |> elem(1) end def current_target(target_config \\ target_config()) do %{os_type: os_type, target_system: target_system} = target_config arch_str = target_system |> Map.values() |> Enum.join("-") case os_type do {:win32, _} -> {:error, "sqlite-vss is not available for architecture: #{arch_str}"} {:unix, osname} -> osname = if osname == :darwin, do: "macos", else: osname case target_config.target_system.arch do "x86_64" -> {:ok, "#{osname}-x86_64"} "aarch64" -> {:ok, "#{osname}-aarch64"} _ -> {:error, "precompiled artifact is not available for this target: \"#{arch_str}\".\nThe available targets are:\n - linux-x86_64\n - macos-aarch64\n - macos-x86_64"} end end end defp http_options(scheme) do http_options() |> maybe_add_proxy_auth(scheme) end defp http_options do # https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/inets cacertfile = cacertfile() |> String.to_charlist() [ ssl: [ verify: :verify_peer, cacertfile: cacertfile, depth: 2, customize_hostname_check: [ match_fun: :public_key.pkix_verify_hostname_match_fun(:https) ] ] ] end defp fetch_body!(url) do scheme = URI.parse(url).scheme url = String.to_charlist(url) Logger.debug("Downloading sqlite-vss from #{url}") {:ok, _} = Application.ensure_all_started(:inets) {:ok, _} = Application.ensure_all_started(:ssl) if proxy = proxy_for_scheme(scheme) do %{host: host, port: port} = URI.parse(proxy) Logger.debug("Using #{String.upcase(scheme)}_PROXY: #{proxy}") set_option = if "https" == scheme, do: :https_proxy, else: :proxy :httpc.set_options([{set_option, {{String.to_charlist(host), port}, []}}]) end options = [body_format: :binary] case :httpc.request(:get, {url, []}, http_options(scheme), options) do {:ok, {{_, 200, _}, _headers, body}} -> body other -> raise """ couldn't fetch #{url}: #{inspect(other)} You may also install the "sqlite_vss" executable manually, \ see the docs: https://hexdocs.pm/sqlite_vss """ end end defp proxy_for_scheme("http") do System.get_env("HTTP_PROXY") || System.get_env("http_proxy") end defp proxy_for_scheme("https") do System.get_env("HTTPS_PROXY") || System.get_env("https_proxy") end defp maybe_add_proxy_auth(http_options, scheme) do case proxy_auth(scheme) do nil -> http_options auth -> [{:proxy_auth, auth} | http_options] end end defp proxy_auth(scheme) do with proxy when is_binary(proxy) <- proxy_for_scheme(scheme), %{userinfo: userinfo} when is_binary(userinfo) <- URI.parse(proxy), [username, password] <- String.split(userinfo, ":") do {String.to_charlist(username), String.to_charlist(password)} else _ -> nil end end defp cacertfile() do Application.get_env(:sqlite_vss, :cacerts_path) || CAStore.file_path() end defp get_url(base_url, target \\ current_target!()) do base_url |> String.replace("$version", current_version()) |> String.replace("$target", target) end # Returns a map with `:arch`, `:vendor`, `:os` and maybe `:abi`. defp system_arch do base = :erlang.system_info(:system_architecture) |> List.to_string() |> String.split("-") triple_keys = case length(base) do 4 -> [:arch, :vendor, :os, :abi] 3 -> [:arch, :vendor, :os] _ -> # It's too complicated to find out, and we won't support this for now. [] end triple_keys |> Enum.zip(base) |> Enum.into(%{}) end end