Current section
Files
Jump to
Current section
Files
mix.exs
defmodule Mix.Tasks.Compile.PHash do
@doc """
If the files don't exist or are older then sources, recompile them.
FIXME: This seems to run every time when tests are run, strangely.
"""
def run(_args) do
priv = Path.join(__DIR__, "priv/")
# Detect platform-specific library extensions
{lib_ext, lib_name} =
case :os.type() do
{:unix, :darwin} -> {".dylib", "libpHash.dylib"}
{:unix, _} -> {".so.1.0.0", "libpHash.so.1.0.0"}
{:win32, _} -> {".dll", "libpHash.dll"}
end
files = [
{"c_lib/pHash/src/pHash.cpp", "#{priv}/#{lib_name}"},
{"c_lib/phash_nifs.cpp", "#{priv}/phash_nifs.so"}
]
should_rebuild =
Enum.any?(
files,
fn {from, result} ->
convert_time = fn {{year, month, day}, {hour, minute, second}} ->
%{
DateTime.now!("Etc/UTC")
| year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second
}
end
not File.exists?(result) or
(
from_dt = convert_time.(File.stat!(from).mtime)
to_dt = convert_time.(File.stat!(result).mtime)
DateTime.compare(from_dt, to_dt) == :gt
)
end
)
if should_rebuild do
# Detect Homebrew paths on macOS
{homebrew_paths, cmake_extra_args} =
case :os.type() do
{:unix, :darwin} ->
# Try to get Homebrew prefix
homebrew_prefix =
case System.cmd("brew", ["--prefix"], stderr_to_stdout: true) do
{prefix, 0} -> String.trim(prefix)
_ -> "/opt/homebrew"
end
# Get paths for required libraries
get_lib_prefix = fn lib_name, default_name ->
case System.cmd("brew", ["--prefix", lib_name], stderr_to_stdout: true) do
{prefix, 0} -> String.trim(prefix)
_ -> "#{homebrew_prefix}/opt/#{default_name}"
end
end
libpng_prefix = get_lib_prefix.("libpng", "libpng")
libjpeg_prefix = get_lib_prefix.("jpeg", "jpeg")
libtiff_prefix = get_lib_prefix.("libtiff", "libtiff")
paths = %{
include: [
"#{libpng_prefix}/include",
"#{libjpeg_prefix}/include",
"#{libtiff_prefix}/include"
],
lib: [
"#{libpng_prefix}/lib",
"#{libjpeg_prefix}/lib",
"#{libtiff_prefix}/lib"
]
}
cmake_args = [
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5",
"-DCMAKE_CXX_FLAGS=-I#{Enum.join(paths.include, " -I")}",
"-DCMAKE_EXE_LINKER_FLAGS=-L#{Enum.join(paths.lib, " -L")}",
"-DCMAKE_SHARED_LINKER_FLAGS=-L#{Enum.join(paths.lib, " -L")}"
]
{paths, cmake_args}
_ ->
{%{include: [], lib: []}, []}
end
with {_, 0} <-
System.cmd(
"cmake",
[
"-DCMAKE_BUILD_TYPE=Release",
"-DBUILD_SHARED_LIBS=FALSE",
"-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
] ++
cmake_extra_args ++ ["."],
cd: "c_lib/pHash",
stderr_to_stdout: true,
into: IO.stream(:stdio, :line)
),
{_, 0} <-
System.cmd(
"cmake",
["--build", ".", "--target", "pHash"],
cd: "c_lib/pHash",
stderr_to_stdout: true,
into: IO.stream(:stdio, :line)
),
phash_lib_path <- find_phash_library("c_lib/pHash", lib_ext),
copy_phash_library(phash_lib_path, priv, lib_name, lib_ext),
erlang_root <-
to_string(:code.root_dir() ++ ~c"/erts-" ++ :erlang.system_info(:version)),
compiler <- detect_compiler(),
compiler_flags <- build_compiler_flags(erlang_root, priv, homebrew_paths, lib_name),
{_, 0} <-
System.cmd(
compiler,
compiler_flags,
cd: "c_lib",
stderr_to_stdout: true,
into: IO.stream(:stdio, :line)
) do
:ok
else
_ -> {:error, ["compilation failed"]}
end
else
:ok
end
end
defp copy_phash_library(source_path, priv_dir, _lib_name, _lib_ext) do
# On macOS with versioned dylib, we need to handle versioning
case :os.type() do
{:unix, :darwin} ->
# Get the directory containing the source library
source_dir = Path.dirname(source_path)
# Look for the actual versioned library file (not symlink)
versioned_lib = Path.join(source_dir, "libpHash.1.0.0.dylib")
if File.exists?(versioned_lib) and File.lstat!(versioned_lib).type != :symlink do
# Copy the versioned library
dest_versioned = Path.join(priv_dir, "libpHash.1.0.0.dylib")
File.cp!(versioned_lib, dest_versioned)
# Create a symlink libpHash.dylib -> libpHash.1.0.0.dylib
dest_symlink = Path.join(priv_dir, "libpHash.dylib")
if File.exists?(dest_symlink), do: File.rm!(dest_symlink)
File.ln_s!("libpHash.1.0.0.dylib", dest_symlink)
else
# Fallback: just copy the source file
File.cp!(source_path, Path.join(priv_dir, "libpHash.dylib"))
end
_ ->
# For Linux/Windows, use the provided lib_name
File.cp!(source_path, Path.join(priv_dir, Path.basename(source_path)))
end
end
defp find_phash_library(base_dir, lib_ext) do
# On macOS, CMake might put the library in different locations
# Also, on macOS the versioned library name is used (e.g., libpHash.1.0.0.dylib)
possible_paths = [
"#{base_dir}/Release/libpHash#{lib_ext}",
"#{base_dir}/Release/libpHash.1.0.0#{lib_ext}",
"#{base_dir}/libpHash#{lib_ext}",
"#{base_dir}/libpHash.1.0.0#{lib_ext}",
"#{base_dir}/Debug/libpHash#{lib_ext}",
"#{base_dir}/Debug/libpHash.1.0.0#{lib_ext}"
]
Enum.find(possible_paths, fn path ->
File.exists?(path)
end) || raise "Could not find compiled pHash library in any expected location"
end
defp detect_compiler do
# Prefer clang++ on macOS, g++ elsewhere
case :os.type() do
{:unix, :darwin} ->
case System.find_executable("clang++") do
nil -> "g++"
path -> path
end
_ ->
"g++"
end
end
defp build_compiler_flags(erlang_root, priv, homebrew_paths, _lib_name) do
base_flags = [
"phash_nifs.cpp",
"-I#{erlang_root}/include",
"-IpHash/src",
"-IpHash/third-party/CImg"
]
# Add Homebrew include paths on macOS
include_flags =
Enum.flat_map(Map.get(homebrew_paths, :include, []), fn path ->
["-I#{path}"]
end)
lib_search_flags =
case :os.type() do
{:unix, :darwin} ->
# On macOS, link against the library in priv and add Homebrew lib paths
lib_paths =
Enum.flat_map(Map.get(homebrew_paths, :lib, []), fn path ->
["-L#{path}"]
end)
lib_paths ++
[
"-L#{priv}",
"-lpHash",
"-lpng",
"-ljpeg",
"-ltiff"
]
_ ->
[
"-LpHash/Release",
"-lpHash"
]
end
link_flags =
case :os.type() do
{:unix, :darwin} ->
[
"-dynamiclib",
"-undefined",
"dynamic_lookup",
"-Wl,-rpath,@loader_path"
]
_ ->
[
"-fpic",
"-shared",
"-Wl,-rpath=#{priv}"
]
end
output_flag = ["-o#{priv}/phash_nifs.so"]
base_flags ++ include_flags ++ lib_search_flags ++ link_flags ++ output_flag
end
end
defmodule PHash.MixProject do
use Mix.Project
def project do
[
app: :phash,
version: "0.1.4",
elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps(),
compilers: [:p_hash] ++ Mix.compilers(),
description: """
Bindings to the C++ pHash library (phash.org).
""",
source_url: "https://github.com/vaartis/phash_ex/",
package: [
maintainers: ["vaartis"],
links: %{
"GitHub" => "https://github.com/vaartis/phash_ex/"
},
licenses: ["GPL-3.0-or-later"],
files:
[
"lib",
"test",
"priv",
"mix.exs",
"README.md",
"LICENSE",
"c_lib/*.cpp",
"c_lib/pHash/COPYING",
"c_lib/pHash/CMakeLists.txt",
"c_lib/pHash/third-party/",
"c_lib/pHash/src/",
# These need to be here because it doesn't build without them
"c_lib/pHash/examples/",
"c_lib/pHash/bindings/CMakeLists.txt"
] ++
Enum.reject(
Path.wildcard("c_lib/pHash/bindings/java/**/*"),
fn path -> Path.extname(path) in [".so", ".java"] or File.dir?(path) end
)
]
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:temp, "~> 0.4"},
{:ex_doc, "~> 0.22", only: :dev},
{:unsafe, "~> 1.0"}
]
end
end