defmodule Evision.MixProject.Metadata do @moduledoc false def app, do: :evision def version, do: "0.2.11" def github_url, do: "https://github.com/cocoa-xu/evision" def opencv_version, do: "4.11.0" # only means compatible. need to write more tests def compatible_opencv_versions, do: ["4.5.3", "4.5.4", "4.5.5", "4.6.0", "4.7.0", "4.8.0", "4.9.0", "4.10.0", "4.11.0"] def default_cuda_version, do: {"12", "9"} def all_cuda_version(_), do: [{"11", "8"}, {"11", "9"}, {"12", "8"}, {"12", "9"}] end defmodule Mix.Tasks.Compile.EvisionPrecompiled do @moduledoc false use Mix.Task require Logger alias Evision.MixProject.Metadata @available_targets [ "aarch64-apple-darwin", "x86_64-apple-darwin", "aarch64-linux-gnu", "aarch64-linux-musl", "x86_64-linux-gnu", "x86_64-linux-musl", "armv7l-linux-gnueabihf", "armv6-linux-gnueabihf", "ppc64le-linux-gnu", "s390x-linux-gnu", "riscv64-linux-gnu", "riscv64-linux-musl", "i686-linux-gnu", "aarch64-windows-msvc", "x86_64-windows-msvc", "aarch64-apple-darwin-ios" ] @compile_nif_version "2.16" def get_available_nif_versions do ["2.16", "2.17"] end def available_targets do opencv_version = System.get_env("OPENCV_VER", Metadata.opencv_version()) available_targets = @available_targets if Enum.member?(["4.11.0"], opencv_version) do available_targets -- ["ppc64le-linux-gnu", "aarch64-windows-msvc"] else available_targets end end def available_nif_urls(_host_nif_version, version \\ Metadata.version()) do nif_version = get_compile_nif_version() Enum.reduce(available_targets(), [], fn target, acc -> no_contrib = get_download_url(target, version, nif_version, false, false, "", "") with_contrib = get_download_url(target, version, nif_version, true, false, "", "") with_cuda = if target in ["x86_64-linux-gnu", "aarch64-linux-gnu", "x86_64-windows-msvc"] do Enum.map(Metadata.all_cuda_version(target), fn {cuda_ver, cudnn_version} -> get_download_url(target, version, nif_version, true, true, cuda_ver, cudnn_version) end) else [] end [no_contrib, with_contrib] ++ with_cuda ++ acc end) end def current_target_nif_url(_host_nif_version, version \\ Metadata.version()) do {target, _} = get_target() nif_version = get_compile_nif_version() enable_contrib = System.get_env("EVISION_ENABLE_CONTRIB", "true") == "true" if enable_contrib do System.put_env("EVISION_ENABLE_CONTRIB", "true") else System.put_env("EVISION_ENABLE_CONTRIB", "false") end enable_cuda = System.get_env("EVISION_ENABLE_CUDA", "false") == "true" {cuda_version, cudnn_version} = if enable_cuda do {defalt_cuda, default_cudnn} = Metadata.default_cuda_version() System.put_env("EVISION_ENABLE_CUDA", "true") { System.get_env("EVISION_CUDA_VERSION", defalt_cuda), System.get_env("EVISION_CUDNN_VERSION", default_cudnn) } else System.put_env("EVISION_ENABLE_CUDA", "false") {"", ""} end get_download_url( target, version, nif_version, enable_contrib, enable_cuda, cuda_version, cudnn_version ) end def checksum_file(app \\ Mix.Project.config()[:app]) when is_atom(app) do # Saves the file in the project root. { Path.join(File.cwd!(), "checksum.exs"), Path.join([File.cwd!(), "checksum.erl"]) } end defp read_checksum_map(app \\ Mix.Project.config()[:app]) when is_atom(app) do {file, _} = checksum_file(app) with {:ok, contents} <- File.read(file), {%{} = contents, _} <- Code.eval_string(contents) do contents else _ -> %{} end end def get_compile_nif_version do @compile_nif_version end def get_nif_version do to_string(:erlang.system_info(:nif_version)) end def get_target do case System.get_env("MIX_TARGET") do "ios" -> {"aarch64-apple-darwin-ios", ["aarch64", "apple", "darwin"]} "xros" -> {"aarch64-apple-darwin-xros", ["aarch64", "apple", "darwin"]} _ -> target = String.split(to_string(:erlang.system_info(:system_architecture)), "-") [arch, os, abi] = case Enum.count(target) do 3 -> target 4 -> [arch, _vendor, os, abi] = target [arch, os, abi] 1 -> with ["win32"] <- target do ["x86_64", "windows", "msvc"] else [unknown_target] -> [unknown_target, "unknown", nil] end end abi = case abi do "darwin" <> _ -> "darwin" "win32" -> {compiler_id, _} = :erlang.system_info(:c_compiler_used) case compiler_id do :msc -> "msvc" _ -> to_string(compiler_id) end _ -> abi end arch = if os == "windows" do case String.downcase(System.get_env("PROCESSOR_ARCHITECTURE")) do "arm64" -> "aarch64" arch when arch in ["x64", "x86_64", "amd64"] -> "x86_64" arch -> arch end else arch end abi = System.get_env("TARGET_ABI", abi) os = System.get_env("TARGET_OS", os) arch = if String.match?(System.get_env("TARGET_CPU", ""), ~r/arm11[357]6/) do "armv6" else System.get_env("TARGET_ARCH", arch) end {Enum.join([arch, os, abi], "-"), [arch, os, abi]} end end def use_precompiled?(log? \\ false) do prefer_precompiled_default = if "dev" in Version.parse!(Metadata.version()).pre do "false" else "true" end prefer_precompiled = System.get_env("EVISION_PREFER_PRECOMPILED", prefer_precompiled_default) use_precompiled? = prefer_precompiled == "true" if log? do if use_precompiled? do Logger.info( "EVISION_PREFER_PRECOMPILED: true; try to download and use the precompiled library." ) else Logger.info( "EVISION_PREFER_PRECOMPILED: #{prefer_precompiled}; will not use precompiled binaries." ) end end use_precompiled? end def available_for_target?(target, log? \\ false) do available_for_target? = Enum.member?(available_targets(), target) if log? do if available_for_target? do Logger.info("Current target `#{target}` has precompiled binaries.") else Logger.warning("Current target `#{target}` does not have precompiled binaries.") end end available_for_target? end def available_for_nif_version?(nif_version, log? \\ false) do host_nif_version = case Enum.count(String.split(nif_version, ".")) do 2 -> Version.parse!("#{nif_version}.0") 3 -> Version.parse!(nif_version) _ -> raise RuntimeError, "unknown nif_version: #{nif_version}" end compile_nif_version = get_compile_nif_version() available_for_nif_version? = Version.compare(Version.parse!("#{compile_nif_version}.0"), host_nif_version) != :gt if log? do if available_for_nif_version? do Logger.info( "Current host NIF version is `#{nif_version}`, will use precompiled binaries with NIF version #{compile_nif_version}." ) else Logger.warning( "Current host NIF version `#{nif_version}` does not have precompiled binaries." ) end end available_for_nif_version? end # https_opts and related code are taken from # https://github.com/elixir-cldr/cldr_utils/blob/master/lib/cldr/http/http.ex @certificate_locations [ # Configured cacertfile System.get_env("ELIXIR_MAKE_CACERT"), # A little hack to use cacerts.pem in CAStore/certfi ## when `:evision` is a dependency in other application ## => Mix.ProjectStack.project_file() ## when `:evision` is the top application ## => __ENV__.file Path.join([ Path.dirname(Mix.ProjectStack.project_file() || __ENV__.file), "deps/castore/priv/cacerts.pem" ]), Path.join([ Path.dirname(Mix.ProjectStack.project_file() || __ENV__.file), "deps/certfi/priv/cacerts.pem" ]), # Debian/Ubuntu/Gentoo etc. "/etc/ssl/certs/ca-certificates.crt", # Fedora/RHEL 6 "/etc/pki/tls/certs/ca-bundle.crt", # 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" ] |> Enum.reject(&is_nil/1) @doc false def certificate_store do @certificate_locations |> Enum.find(&File.exists?/1) |> warning_if_no_cacertfile! |> :erlang.binary_to_list() end defp warning_if_no_cacertfile!(nil) do Logger.warning(""" No certificate trust store was found. Tried looking for: #{inspect(@certificate_locations)} A certificate trust store is required in order to download locales for your configuration. Since elixir_make could not detect a system installed certificate trust store one of the following actions may be taken: 1. Install the hex package `castore`. It will be automatically detected after recompilation. 2. Install the hex package `certifi`. It will be automatically detected after recomilation. 3. Specify the location of a certificate trust store by configuring it in environment variable: export ELIXIR_MAKE_CACERT="/path/to/cacerts.pem" """) "" end defp warning_if_no_cacertfile!(file) do file end defp https_opts(hostname) do cert_file = certificate_store() if secure_ssl?() and cert_file != [] do [ ssl: [ verify: :verify_peer, cacertfile: certificate_store(), 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) ] ] ] else [ ssl: [ verify: :verify_none, server_name_indication: hostname, secure_renegotiate: true, reuse_sessions: true, versions: protocol_versions(), ciphers: preferred_ciphers() ] ] end end def preferred_ciphers do preferred_ciphers = [ # 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(preferred_ciphers, []) end def protocol_versions do if otp_version() < 25 do [:"tlsv1.2"] else [:"tlsv1.2", :"tlsv1.3"] end end def otp_version do :erlang.system_info(:otp_release) |> List.to_integer() end def preferred_eccs do # TLS curves: X25519, prime256v1, secp384r1 preferred_eccs = [:secp256r1, :secp384r1] :ssl.eccs() -- :ssl.eccs() -- preferred_eccs end def secure_ssl? do case System.get_env("ELIXIR_MAKE_UNSAFE_HTTPS") do nil -> true "FALSE" -> false "false" -> false "nil" -> false "NIL" -> false _other -> true end end def download!(url, save_as, overwrite \\ false) def download!(url, save_as, false) do unless File.exists?(save_as) do download!(url, save_as, true) end checksum(save_as) end def download!(url, save_as, true) do with {:ok, body} <- download!(url) do File.write!(save_as, body) checksum(save_as) end end def download!(url) do opts = [body_format: :binary] arg = {url, []} # https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/inets # TODO: This may no longer be necessary from Erlang/OTP 26.0 or later. http_options = https_opts(String.to_charlist(URI.parse(url).host)) if proxy = System.get_env("HTTP_PROXY") || System.get_env("http_proxy") do Mix.shell().info("Using HTTP_PROXY: #{proxy}") %{host: host, port: port} = URI.parse(proxy) :httpc.set_options([{:proxy, {{String.to_charlist(host), port}, []}}]) end if proxy = System.get_env("HTTPS_PROXY") || System.get_env("https_proxy") do Mix.shell().info("Using HTTPS_PROXY: #{proxy}") %{host: host, port: port} = URI.parse(proxy) :httpc.set_options([{:https_proxy, {{String.to_charlist(host), port}, []}}]) end Mix.shell().info("Downloading precompiled tarball from: #{url}") case :httpc.request(:get, arg, http_options, opts) do {:ok, {{_, 200, _}, _, body}} -> {:ok, body} {:error, reason} -> raise RuntimeError, "Cannot download file from #{url}: #{reason}." end end def filename( target, version, nif_version, enable_contrib, enable_cuda, cuda_version, cudnn_version, with_ext \\ "" ) def filename( target, version, nif_version, _enable_contrib = false, _enable_cuda, _cuda_version, _cudnn_version, with_ext ) do "evision-nif_#{nif_version}-#{target}-#{version}#{with_ext}" end def filename( target, version, nif_version, _enable_contrib = true, _enable_cuda = false, _cuda_version, _cudnn_version, with_ext ) do "evision-nif_#{nif_version}-#{target}-contrib-#{version}#{with_ext}" end def filename( target, version, nif_version, _enable_contrib = true, _enable_cuda = true, cuda_version, cudnn_version, with_ext ) do "evision-nif_#{nif_version}-#{target}-contrib-cuda#{cuda_version}-cudnn#{cudnn_version}-#{version}#{with_ext}" end def get_download_url( target, version, nif_version, enable_contrib, enable_cuda, cuda_version, cudnn_version ) do tar_file = filename( target, version, nif_version, enable_contrib, enable_cuda, cuda_version, cudnn_version, ".tar.gz" ) "#{Metadata.github_url()}/releases/download/v#{version}/#{tar_file}" end def cache_dir do cache_opts = if System.get_env("MIX_XDG"), do: %{os: :linux}, else: %{} cache_dir = :filename.basedir(:user_cache, "", cache_opts) cache_dir = System.get_env("ELIXIR_MAKE_CACHE_DIR", cache_dir) File.mkdir_p!(cache_dir) cache_dir end @checksum_algo :sha256 def checksum_algo, do: @checksum_algo def checksum(file_path, algo \\ @checksum_algo) do case File.read(file_path) do {:ok, content} -> file_hash = algo |> :crypto.hash(content) |> Base.encode16(case: :lower) {:ok, "#{algo}", "#{file_hash}"} {:error, reason} -> {:error, "cannot read the file for checksum comparison: #{inspect(file_path)}. " <> "Reason: #{inspect(reason)}"} end end def app_priv(app \\ Metadata.app()) when is_atom(app) do build_path = Mix.Project.build_path() Path.join([build_path, "lib", "#{app}", "priv"]) end def prepare( target, os, version, nif_version, enable_contrib, enable_cuda, cuda_version, cudnn_version ) do name = filename( target, version, nif_version, enable_contrib, enable_cuda, cuda_version, cudnn_version ) filename = filename( target, version, nif_version, enable_contrib, enable_cuda, cuda_version, cudnn_version, ".tar.gz" ) cache_dir = cache_dir() cache_file = Path.join([cache_dir, filename]) unarchive_dest_dir = Path.join([cache_dir, name]) cached_priv_dir = Path.join([cache_dir, name, "priv"]) cached_elixir_dir = Path.join([cache_dir, name, "elixir_generated"]) evision_so_file = if os == "windows" do "evision.dll" else "evision.so" end windows_fix_so_file = if os == "windows" do "windows_fix.dll" else "windows_fix.so" end evision_so_file = Path.join([app_priv(), evision_so_file]) windows_fix_so_file = Path.join([app_priv(), windows_fix_so_file]) # first we check if we already have the NIF file # if not, then we defintely need to copy it (and other files) from somewhere needs_copy = !File.exists?(evision_so_file) or !File.exists?(windows_fix_so_file) if needs_copy do # to copy the nif file and other files # we first check if they are cached in the cache dir if !File.dir?(unarchive_dest_dir) do # if `unarchive_dest_dir` does not exists # then perhaps we need to extract these files from the precompiled binary tarball # # to extract files from the tarball # we check if the tarball is cached in the cache dir {needs_download, needs_unarchive} = if File.exists?(cache_file) do # and it has to be a regular file if File.regular?(cache_file) do # of course we have to compute its checksum {:ok, algo, cache_file_checksum} = checksum(cache_file) Logger.info( "Precompiled binary tarball cached at #{cache_file}, #{algo}=#{cache_file_checksum}" ) # and verify the checksum in the map {checksum_matched?, algo_in_map, checksum_in_map} = verify_checksum(cache_file, algo, cache_file_checksum) if checksum_matched? do # if these checksum matches # we can extract files from the tarball {false, true} else Logger.error( "Checksum mismatched: #{cache_file}[#{algo}=#{cache_file_checksum}], expected:[#{algo_in_map}=#{checksum_in_map}]" ) Logger.warning("Will delete cached tarball #{cache_file} and re-download") # otherwise we delete the cached file and download it again {true, true} end else # not a regular file # remove it and download again File.rm_rf!(cache_file) Logger.warning( "Cached file at #{cache_file} is not a regular file, will delete it and re-download" ) {true, true} end else {true, true} end if needs_download do download_url = get_download_url( target, version, nif_version, enable_contrib, enable_cuda, cuda_version, cudnn_version ) {:ok, _} = Application.ensure_all_started(:inets) {:ok, _} = Application.ensure_all_started(:ssl) # of course we have to verify its checksum too {:ok, algo, checksum} = download!(download_url, cache_file, true) Logger.info( "Precompiled binary tarball downloaded and saved to #{cache_file}, #{algo}=#{checksum}" ) {checksum_matched?, algo_in_map, checksum_in_map} = verify_checksum(cache_file, algo, checksum) if !checksum_matched? do msg = "Checksum mismatched: downloaded file: #{cache_file}[#{algo}=#{checksum}], expected:[#{algo_in_map}=#{checksum_in_map}]" Logger.error(msg) raise RuntimeError, msg end end if needs_unarchive do File.rm_rf!(unarchive_dest_dir) unarchive!(cache_file, cache_dir) end end # if `unarchive_dest_dir` exists and is a directory # then we can simply copy they to the expected locations deploy_from_dir!(cached_priv_dir, cached_elixir_dir) else :ok end end def verify_checksum(cache_file, algo, cache_file_checksum) do checksum_map = read_checksum_map() basename = Path.basename(cache_file) case Map.fetch(checksum_map, basename) do {:ok, algo_with_hash} -> [algo_in_map, hash] = String.split(algo_with_hash, ":") {cache_file_checksum == hash and algo_in_map == algo, algo_in_map, hash} :error -> {:error, "the precompiled NIF file does not exist in the checksum file. " <> "Please consider run: `EVISION_FETCH_PRECOMPILED=true mix evision.fetch --all` to generate the checksum file."} end end def deploy_from_dir!(cached_priv_dir, cached_elixir_dir) do app_priv = app_priv() generated_elixir_dir = Path.join([File.cwd!(), "lib", "generated"]) if File.exists?(app_priv) do File.rm_rf!(app_priv) end if File.exists?(generated_elixir_dir) do File.rm_rf!(generated_elixir_dir) end Logger.info("Copying priv directory: #{cached_priv_dir} => #{app_priv}") with {:ok, _} <- File.cp_r(cached_priv_dir, app_priv) do :ok else error -> msg = "Failed to copy priv directory, `File.cp_r`: #{inspect(error)}" Logger.error(msg) raise RuntimeError, msg end Logger.info( "Copying generated Elixir binding files: #{cached_elixir_dir} => #{generated_elixir_dir}" ) with {:ok, _} <- File.cp_r(cached_elixir_dir, generated_elixir_dir) do :ok else error -> msg = "Failed to copy generated Elixir binding files, `File.cp_r`: #{inspect(error)}" Logger.error(msg) raise RuntimeError, msg end end def unarchive!(filepath, to_directory) do File.mkdir_p!(to_directory) with :ok <- :erl_tar.extract(filepath, [:compressed, {:cwd, to_directory}]) do :ok else err -> msg = "Failed to unarchive tarball file: #{filepath}, error: #{inspect(err)}" Logger.error(msg) raise RuntimeError, msg end end def deploy_type(log? \\ false) do {target, [_arch, _os, abi]} = get_target() {:ok, %Version{pre: pre}} = Version.parse(Metadata.version()) nif_version = get_nif_version() if use_precompiled?(log?) and pre != ["dev"] and available_for_target?(target, log?) and available_for_nif_version?(nif_version, log?) do {:precompiled, abi} else {:build_from_source, abi} end end @impl true def run(_args) do {target, [_arch, os, _abi]} = get_target() evision_so_file = if os == "windows" do "evision.dll" else "evision.so" end windows_fix_so_file = if os == "windows" do "windows_fix.dll" else "windows_fix.so" end evision_so_file = Path.join([app_priv(), evision_so_file]) windows_fix_so_file = Path.join([app_priv(), windows_fix_so_file]) if !File.exists?(evision_so_file) or !File.exists?(windows_fix_so_file) do with {:precompiled, _} <- deploy_type(true) do version = Metadata.version() nif_version = get_compile_nif_version() enable_contrib = System.get_env("EVISION_ENABLE_CONTRIB", "true") == "true" enable_cuda = System.get_env("EVISION_ENABLE_CUDA", "false") == "true" {default_cuda_version, default_cudnn_version} = Metadata.default_cuda_version() cuda_version = System.get_env("EVISION_CUDA_VERSION", default_cuda_version) cudnn_version = System.get_env("EVISION_CUDNN_VERSION", default_cudnn_version) prepare( target, os, version, nif_version, enable_contrib, enable_cuda, cuda_version, cudnn_version ) else _ -> raise RuntimeError, "Cannot use precompiled binaries." end else :ok end end end defmodule Evision.MixProject do use Mix.Project require Logger alias Evision.MixProject.Metadata alias Mix.Tasks.Compile.EvisionPrecompiled @source_url "#{Metadata.github_url()}/tree/v#{Metadata.version()}" def project do {compilers, make_env} = if System.get_env("EVISION_FETCH_PRECOMPILED") != "true" do {deploy_type, target_abi} = EvisionPrecompiled.deploy_type(false) if deploy_type == :build_from_source do {cmake_options, enabled_modules} = generate_cmake_options() make_env = %{ "HAVE_NINJA" => "#{System.find_executable("ninja") != nil}", "OPENCV_VER" => opencv_versions(System.get_env("OPENCV_VER", Metadata.opencv_version())), "MAKE_BUILD_FLAGS" => System.get_env("MAKE_BUILD_FLAGS", "-j#{System.schedulers_online()}"), "CMAKE_OPTIONS" => cmake_options, "ENABLED_CV_MODULES" => enabled_modules, "TARGET_ABI" => System.get_env("TARGET_ABI", target_abi), "EVISION_GENERATE_LANG" => System.get_env("EVISION_GENERATE_LANG", "elixir"), "EVISION_PREFER_PRECOMPILED" => "false" } make_env = case :os.type() do {:win32, _} -> Map.put_new( make_env, "PYTHON3_EXECUTABLE", System.find_executable("python3") || System.find_executable("python") ) _ -> make_env end {[:elixir_make] ++ Mix.compilers(), make_env} else {[:evision_precompiled] ++ Mix.compilers(), %{}} end else {Mix.compilers(), %{}} end [ app: Metadata.app(), name: "Evision", version: Metadata.version(), elixir: "~> 1.11", deps: deps(), docs: docs(), aliases: aliases(), elixirc_paths: elixirc_paths(), erlc_paths: erlc_paths(), compilers: compilers, source_url: Metadata.github_url(), description: description(), package: package(), make_executable: make_executable(), make_makefile: make_makefile(), make_env: make_env, xref: [ exclude: [ :wx, :wxBitmap, :wxBoxSizer, :wxDC, :wxFrame, :wxImage, :wxMemoryDC, :wxPanel, :wxSizer, :wxStaticBoxSizer, :wxWindowDC, :wx_object ] ] ] end defp aliases do [ compile_opencv: &compile_opencv/1 ] end defp compile_opencv(_) do config = Mix.Project.config() Mix.shell().print_app() priv? = File.dir?("priv") Mix.Project.ensure_structure() config = Keyword.put_new(config, :make_targets, ["opencv"]) compiler_file = Path.join([ Path.dirname(Mix.ProjectStack.project_file() || __ENV__.file), "deps/elixir_make/lib/elixir_make/compiler.ex" ]) with [{ElixirMake.Compiler, _}] <- Code.require_file(compiler_file) do ElixirMake.Compiler.make(config, []) # IF there was no priv before and now there is one, we assume # the user wants to copy it. If priv already existed and was # written to it, then it won't be copied if build_embedded is # set to true. if not priv? and File.dir?("priv") do Mix.Project.build_structure() end {:ok, []} else _ -> Mix.shell().error("Cannot compile OpenCV only: failed to require file #{compiler_file}") end end def make_executable() do case :os.type() do {:win32, _} -> "nmake" _ -> "make" end end def make_makefile() do case :os.type() do {:win32, _} -> "Makefile.win" _ -> "Makefile" end end def opencv_versions(version) do if Enum.member?(Metadata.compatible_opencv_versions(), version) do version else Logger.warning( "OpenCV version #{version} is not in the compatible list, you may encounter compile errors" ) Logger.warning( "Compatible OpenCV versions: " <> (Metadata.compatible_opencv_versions() |> Enum.join(", ")) ) version end end def application do [ extra_applications: [:logger, :inets, :ssl], mod: {Evision.Application, []} ] end @module_configuration %{ # opencv/opencv_contrib opencv: [ # module name: is_enabled # note that these true values here only mean that we requested # these module to be compiled # some of them can be disabled due to dependency issues calib3d: true, core: true, dnn: true, features2d: true, flann: true, highgui: true, imgcodecs: true, imgproc: true, ml: true, photo: true, stitching: true, ts: true, video: true, videoio: true, gapi: false, world: false, python2: false, python3: false, java: false ], opencv_contrib: [ aruco: true, barcode: true, bgsegm: true, bioinspired: true, dnn_superres: true, face: true, hfs: true, img_hash: true, line_descriptor: true, mcc: true, plot: true, quality: true, rapid: true, reg: true, rgbd: true, saliency: true, shape: true, stereo: true, structured_light: true, surface_matching: true, text: true, tracking: true, wechat_qrcode: System.get_env("MIX_TARGET") != "ios" or System.get_env("MIX_TARGET") != "xros", xfeatures2d: true, ximgproc: true, xphoto: true, # no bindings yet datasets: false, dnn_objdetect: false, dpm: false, optflow: false, sfm: false, videostab: false, xobjdetect: false ], cuda: [ cudaarithm: true, cudabgsegm: true, cudacodec: true, cudafeatures2d: true, cudafilters: true, cudaimgproc: true, cudalegacy: true, cudaobjdetect: true, cudaoptflow: true, cudastereo: true, cudawarping: true, cudev: true ] } defp module_configuration, do: @module_configuration @enabled_img_codecs (if System.get_env("MIX_TARGET") == "ios" or System.get_env("MIX_TARGET") == "xros" do [] else [:png, :jpeg, :tiff, :webp, :openjpeg, :jasper, :openexr] end) # To make things easier, you can set `compile_mode` to `only_enabled_modules` so # that only modules specified in `enabled_modules` will be compiled. Like-wise, # set `except_disabled_modules` to only exclude modules in `disabled_modules`. # By default, the value of `compile_mode` is `auto`, which means to leave unspecified # modules to CMake to decide. @compile_mode :auto defp generate_cmake_options() do mc = module_configuration() enable_cuda = System.get_env("EVISION_ENABLE_CUDA", "false") enable_opencv_cuda = enable_cuda == "true" if enable_opencv_cuda do System.put_env("EVISION_ENABLE_CUDA", "true") else System.put_env("EVISION_ENABLE_CUDA", "false") end enable_contrib = System.get_env("EVISION_ENABLE_CONTRIB", "true") enable_opencv_contrib = enable_contrib == "true" if enable_opencv_cuda and !enable_opencv_contrib do Logger.warning( "EVISION_ENABLE_CUDA is set to true, while EVISION_ENABLE_CONTRIB is set to false. CUDA support will NOT be available." ) end if enable_opencv_contrib do System.put_env("EVISION_ENABLE_CONTRIB", "true") else System.put_env("EVISION_ENABLE_CONTRIB", "false") end all_modules = Enum.map(mc.opencv, fn {m, _} -> m end) ++ Enum.map(mc.opencv_contrib, fn {m, _} -> m end) enabled_modules = Enum.filter(mc.opencv, fn {_, e} -> e end) ++ if enable_opencv_contrib do Enum.filter(mc.opencv_contrib, fn {_, e} -> e end) else [] end ++ if enable_opencv_cuda do Enum.filter(mc.cuda, fn {_, e} -> e end) else [] end disabled_modules = Enum.filter(mc.opencv, fn {_, e} -> !e end) ++ if enable_opencv_contrib do Enum.filter(mc.opencv_contrib, fn {_, e} -> !e end) else [] end enabled_modules = Keyword.keys(enabled_modules) disabled_modules = Keyword.keys(disabled_modules) enabled_img_codecs = Application.get_env(:evision, :enabled_img_codecs, @enabled_img_codecs) compile_mode = Application.get_env(:evision, :compile_mode, @compile_mode) {cmake_options, enabled_modules} = case compile_mode do :auto -> cmake_options = (enabled_modules |> Enum.map(&"-D BUILD_opencv_#{Atom.to_string(&1)}=ON") |> Enum.join(" ")) <> " " <> (disabled_modules |> Enum.map(&"-D BUILD_opencv_#{&1}=OFF") |> Enum.join(" ")) {cmake_options, enabled_modules} :only_enabled_modules -> cmake_options = "-D BUILD_LIST=" <> (enabled_modules |> Enum.map(&Atom.to_string(&1)) |> Enum.join(",")) {cmake_options, enabled_modules} :except_disabled_modules -> enabled_modules = all_modules -- disabled_modules cmake_options = "-D BUILD_LIST=" <> (enabled_modules |> Enum.map(&Atom.to_string(&1)) |> Enum.join(",")) {cmake_options, enabled_modules} unrecognised_mode -> Mix.raise("unrecognised compile_mode for evision: #{inspect(unrecognised_mode)}") end options = cmake_options <> " " <> (enabled_img_codecs |> Enum.map(&"-D BUILD_#{Atom.to_string(&1) |> String.upcase()}=ON") |> Enum.join(" ")) <> " " options = if enable_opencv_cuda and enable_opencv_contrib do "#{options} -D WITH_CUDA=ON" else options end {options, enabled_modules |> Enum.map(&Atom.to_string(&1)) |> Enum.join(",")} end defp deps do [ # compilation {:elixir_make, "~> 0.7", runtime: false}, {:castore, "~> 0.1 or ~> 1.0"}, # runtime {:nx, "~> 0.6"}, # optional ## kino: for smart cells and better output in livebook {:kino, "~> 0.11", optional: true}, ## progress_bar: for the model zoo smart cell {:progress_bar, "~> 2.0 or ~> 3.0", optional: true}, # docs {:ex_doc, "~> 0.34.0", only: :docs, runtime: false}, # test {:scidata, "~> 0.1", only: :test} ] end defp docs do [ main: "Evision", source_ref: "v#{Metadata.version()}", source_url: @source_url, extras: [ "CHANGELOG.md", "Cheatsheet.cheatmd", "README.md", # Evision "examples/getting_started.livemd", "examples/qrcode.livemd", "examples/warp_perspective.livemd", "examples/warp_polar.livemd", "examples/find_and_draw_contours.livemd", "examples/stitching.livemd", "examples/pca.livemd", "examples/photo-hdr.livemd", "examples/sudoku.livemd", "examples/cifar10.livemd", # Evision.DNN "examples/dnn-googlenet.livemd", "examples/dnn-detection-model.livemd", "examples/densenet121_benchmark.livemd", # Evision.ML "examples/ml-svm.livemd", "examples/ml-decision_tree_and_random_forest.livemd" ], before_closing_body_tag: &before_closing_body_tag/1, groups_for_docs: [ external: &(&1[:namespace] == :external), Enumerator: &(&1[:enum] == true) ] ] end defp before_closing_body_tag(:html) do ~S' ' end defp before_closing_body_tag(_), do: "" defp description() do "OpenCV-Erlang/Elixir binding." end def links do %{ "GitHub" => Metadata.github_url(), "Readme" => "#{Metadata.github_url()}/blob/v#{Metadata.version()}/README.md", "Changelog" => "#{Metadata.github_url()}/blob/v#{Metadata.version()}/CHANGELOG.md" } end defp elixirc_paths, do: ["lib"] defp erlc_paths, do: [] defp package() do [ name: "evision", # These are the default files included in the package files: ~w( c_src/evision_custom_headers c_src/modules c_src/windows_fix c_src/ArgInfo.hpp c_src/erlcompat.hpp c_src/evision_consts.h c_src/evision_custom_headers.h c_src/evision.cpp c_src/nif_utils.hpp cc_toolchain lib/assets lib/evision lib/mix lib/smartcell lib/zoo lib/*.ex patches py_src/*.py scripts src/evision_mat.erl src/evision_highgui.erl src/evision_internal_structurise.erl src/evision.app.src checksum.exs checksum.erl CMakeLists.txt evision_precompiled.erl Makefile Makefile.win .formatter.exs mix.exs rebar.config README* LICENSE* CHANGELOG*), licenses: ["Apache-2.0"], links: links() ] end end