-module(tflite_beam_precompiled). -export([is_precompiled_binary_available/0, install_precompiled_binary_if_available/0]). -define(PRECOMPILED_TARBALL_NAME, "tflite_beam-nif-~s-~s-v~s"). -define(PRECOMPILED_DOWNLOAD_URL, "https://github.com/cocoa-xu/tflite_beam/releases/download/v~s/~s"). -define(TFLITE_BEAM_SO_FILE, "priv/tflite_beam.so"). -define(TFLITE_BEAM_DLL_FILE, "priv/tflite_beam.dll"). -define(LIBEDGETPU_RUNTIME_VERSION, "0.1.6"). -include_lib("kernel/include/file.hrl"). app_version() -> {ok, Cwd} = file:get_cwd(), Src = filename:join([Cwd, "src", "tflite_beam.app.src"]), case file:read_file(Src) of {ok, BinContent} -> Content = binary_to_list(BinContent), case erl_scan:string(Content) of {ok, Tokens, _} -> case erl_parse:parse_term(Tokens) of {ok, {application, tflite_beam, App}} -> case proplists:get_value(vsn, App) of undefined -> "unknown"; Version -> case is_list(Version) of true -> Version; false -> "unknown" end end; {error, _} -> "unknown" end; {error, _} -> "unknown" end; {error, _} -> "unknown" end. is_dev() -> AppVersion = app_version(), case string:find(AppVersion, "-dev") of nomatch -> {false, AppVersion}; _ -> {true, AppVersion} end. only_darwin(DarwinABI) -> case string:prefix(DarwinABI, "darwin") of nomatch -> DarwinABI; _ -> "darwin" end. maybe_override_by_env(EnvName, Default) -> case os:getenv(EnvName) of false -> Default; Value -> Value end. get_target() -> TargetParts = string:split(erlang:system_info(system_architecture), "-", all), [G_ARCH, G_OS, G_ABI] = case length(TargetParts) of 4 -> [T_ARCH, _, T_OS, T_ABI] = TargetParts, [T_ARCH, T_OS, only_darwin(T_ABI)]; 3 -> [T_ARCH, T_OS, T_ABI] = TargetParts, [T_ARCH, T_OS, only_darwin(T_ABI)]; 1 -> case TargetParts of ["win32"] -> WIN_ARCH = case maybe_override_by_env("PROCESSOR_ARCHITECTURE", "x86_64") of "ARM64" -> "aarch64"; PROCESSOR_ARCHITECTURE -> PROCESSOR_ARCHITECTURE end, [WIN_ARCH, "windows", "msvc"]; _ -> ["unknown", "unknown", "unknown"] end end, ARCH = maybe_override_by_env("TARGET_ARCH", G_ARCH), CPU = maybe_override_by_env("TARGET_CPU", ""), FinalARCH = case ARCH of "arm" -> case CPU of "arm1176jzf_s" -> "armv6"; _ -> IsCortex = string:substr(CPU, 1, 7) =:= "cortex_", if IsCortex -> "armv7l"; true -> ARCH end end; _ -> ARCH end, OS = maybe_override_by_env("TARGET_OS", G_OS), ABI = maybe_override_by_env("TARGET_ABI", G_ABI), TRIPLET = io_lib:fwrite("~s-~s-~s", [FinalARCH, OS, ABI]), TRIPLET. get_nif_version() -> erlang:system_info(nif_version). is_precompiled_binary_available() -> case is_dev() of {true, _} -> false; {false, AppVersion} -> Target = get_target(), NifVersion = get_nif_version(), Name = lists:flatten(io_lib:fwrite(?PRECOMPILED_TARBALL_NAME, [NifVersion, Target, AppVersion])), TarballFilename = lists:flatten(io_lib:fwrite("~s.tar.gz", [Name])), TarballURL = lists:flatten(io_lib:fwrite(?PRECOMPILED_DOWNLOAD_URL, [AppVersion, TarballFilename])), {true, Name, TarballFilename, TarballURL} end. cache_opts() -> case os:getenv("MIX_XDG") of false -> #{}; _ -> #{os => linux} end. cache_path(Filename) -> CacheBaseDir = filename:basedir(user_cache, "", cache_opts()), CacheDir = maybe_override_by_env("ELIXIR_MAKE_CACHE_DIR", CacheBaseDir), file:make_dir(CacheDir), Filepath = filename:join([CacheDir, Filename]), {filelib:is_file(Filepath), Filepath}. download_precompiled_binary(URL, CacheTo) -> case do_download(URL) of {ok, Body} -> file:write_file(CacheTo, Body), FullPath = CacheTo, io:fwrite("[INFO] Precompiled binary tarball downloaded and saved to ~s~n", [FullPath]), {ok, FullPath}; {error, DownloadError} -> io:fwrite("[ERROR] Cannot download precompiled binary from ~p: ~p~n", [URL, DownloadError]), {error, CacheTo} end. download(URL, CacheFilename) -> {Exists, CacheTo} = cache_path(CacheFilename), if Exists -> io:fwrite("[INFO] Precompiled binary tarball cached at ~s\r\n", [CacheTo]), {ok, CacheTo}; true -> io:fwrite("[INFO] not downloaded, will download!~n"), download_precompiled_binary(URL, CacheTo) end. certificate_store() -> PossibleLocations = [ %% Configured cacertfile os:getenv("ELIXIR_MAKE_CACERT"), %% Debian/Ubuntu/Gentoo etc. "/etc/ssl/certs/ca-certificates.crt", %% Fedora/RHEL 6 "/etc/pki/tls/certs/ca-bundle.crt", %5 OpenSUSE "/etc/ssl/ca-bundle.pem", %% OpenELEC "/etc/pki/tls/cacert.pem", %% CentOS/RHEL 7 "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", %% Open SSL on MacOS "/usr/local/etc/openssl/cert.pem", %% MacOS & Alpine Linux "/etc/ssl/cert.pem" ], CheckExistance = lists:map(fun (F) -> {filelib:is_file(F), F} end, PossibleLocations), ExistingOnes = lists:dropwhile(fun ({X, _}) -> X == false end, CheckExistance), case length(ExistingOnes) of Len when Len > 0 -> {_, Cert} = hd(ExistingOnes), Cert; _ -> io:fwrite("[WARNING] Cannot find CA certificate store in default locations: ~p~n", [PossibleLocations]), io:fwrite("You can set environment variable ELIXIR_MAKE_CACERT to the SSL cert on your system.~n"), nil end. preferred_ciphers() -> PreferredCiphers = [ %% Cipher suites (TLS 1.3): TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 #{cipher => aes_128_gcm, key_exchange => any, mac => aead, prf => sha256}, #{cipher => aes_256_gcm, key_exchange => any, mac => aead, prf => sha384}, #{cipher => chacha20_poly1305, key_exchange => any, mac => aead, prf => sha256}, %% Cipher suites (TLS 1.2): ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256: %% ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305: %% ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 #{cipher => aes_128_gcm, key_exchange => ecdhe_ecdsa, mac => aead, prf => sha256}, #{cipher => aes_128_gcm, key_exchange => ecdhe_rsa, mac => aead, prf => sha256}, #{cipher => aes_256_gcm, key_exchange => ecdh_ecdsa, mac => aead, prf => sha384}, #{cipher => aes_256_gcm, key_exchange => ecdh_rsa, mac => aead, prf => sha384}, #{cipher => chacha20_poly1305, key_exchange => ecdhe_ecdsa, mac => aead, prf => sha256}, #{cipher => chacha20_poly1305, key_exchange => ecdhe_rsa, mac => aead, prf => sha256}, #{cipher => aes_128_gcm, key_exchange => dhe_rsa, mac => aead, prf => sha256}, #{cipher => aes_256_gcm, key_exchange => dhe_rsa, mac => aead, prf => sha384} ], ssl:filter_cipher_suites(PreferredCiphers, []). protocol_versions() -> case list_to_integer(erlang:system_info(otp_release)) of Version when Version < 25 -> ['tlsv1.2']; _ -> ['tlsv1.2', 'tlsv1.3'] end. preferred_eccs() -> %% TLS curves: X25519, prime256v1, secp384r1 PreferredECCS = [secp256r1, secp384r1], ssl:eccs() -- ssl:eccs() -- PreferredECCS. secure_ssl() -> case os:getenv("ELIXIR_MAKE_UNSAFE_HTTPS") of nil -> true; "FALSE" -> false; "false" -> false; "nil" -> false; "NIL" -> false; _ -> true end. https_opts(Hostname) -> CertFile = certificate_store(), case {secure_ssl(), is_list(CertFile)} of {true, true} -> [ { ssl, [ {verify, verify_peer}, {cacertfile, CertFile}, {depth, 4}, {ciphers, preferred_ciphers()}, {versions, protocol_versions()}, {eccs, preferred_eccs()}, {reuse_sessions, true}, {server_name_indication, Hostname}, {secure_renegotiate, true}, {customize_hostname_check, [ {match_fun, public_key:pkix_verify_hostname_match_fun(https)} ]} ] } ]; _ -> [ { ssl, [ {verify, verify_none}, {ciphers, preferred_ciphers()}, {versions, protocol_versions()}, {reuse_sessions, true}, {server_name_indication, Hostname}, {secure_renegotiate, true} ] } ] end. do_download(URL) -> application:ensure_started(inets), ssl:start(), HttpOtps = https_opts("github.com"), Request = {URL, []}, case httpc:request(get, Request, HttpOtps, [{body_format, binary}]) of {ok, {{_, 200, _}, _, Body}} -> {ok, Body}; {error, Reason} -> {error, Reason}; Err -> {error, lists:flatten(io_lib:fwrite("Cannot download file from ~s: ~p~n", [URL, Err]))} end. is_already_installed() -> case string:find(get_target(), "windows-msvc") of nomatch -> filelib:is_regular(?TFLITE_BEAM_SO_FILE); _ -> filelib:is_regular(?TFLITE_BEAM_DLL_FILE) end. install_precompiled_binary_if_available() -> case is_already_installed() of false -> case is_precompiled_binary_available() of {true, Name, TarballFilename, TarballURL} -> case download(TarballURL, TarballFilename) of {error, _} -> exit(failed); {ok, TarballFileFullPath} -> file:del_dir_r("tmp_priv"), Status = case erl_tar:extract(TarballFileFullPath, [compressed, {cwd, "tmp_priv"}]) of ok -> file:del_dir_r("priv"), TmpPriv = filename:join(["tmp_priv", Name, "priv"]), PrivRenameOk = file:rename(TmpPriv, "priv"), case PrivRenameOk of {error, PrivError} -> io:fwrite("[ERROR] Failed to move priv directory: ~p~n", [PrivError]), failed; _ -> ok end; Error -> io:fwrite("[ERROR] Failed to unarchive tarball file: ~s, error: ~p~n", [TarballFileFullPath, Error]), failed end, file:del_dir_r("tmp_priv"), case Status of failed -> exit(failed); _ -> ok end end; What -> io:fwrite("[INFO] Cannot find precompiled binary: ~p~n", [What]), exit(failed) end; true -> ok end.