# Makefile for llama_cpp_ex NIF
# Called by elixir_make during `mix compile`

PREFIX = $(MIX_APP_PATH)/priv
BUILD  = $(MIX_APP_PATH)/obj
NIF_SO = $(PREFIX)/llama_cpp_ex_nif.so

# --- llama.cpp source tree ---------------------------------------------------
# A git checkout has this as the `vendor/llama.cpp` submodule. A Hex tarball does
# not: shipping the tree would add hundreds of MB to the package, so `files:` in
# mix.exs ships `.gitmodules` instead and the rule near the bottom of this file
# clones the tree on demand, pinned to LLAMA_COMMIT.
LLAMA_DIR = $(shell pwd)/vendor/llama.cpp

# Upstream URL, read from .gitmodules so there is a single source of truth. The
# literal is only a fallback for the case where .gitmodules is missing.
LLAMA_REPO := $(shell git config --file .gitmodules --get submodule.vendor/llama.cpp.url 2>/dev/null)
ifeq ($(strip $(LLAMA_REPO)),)
  LLAMA_REPO := https://github.com/ggml-org/llama.cpp
endif

# Pinned llama.cpp commit, used when vendor/llama.cpp has to be cloned. MUST
# match the vendor/llama.cpp submodule; bump both together, see
# docs/release-guide.md. Override to build the NIF against another revision.
LLAMA_COMMIT ?= ddd4ec1428a6201e18975ea52b07c71e0f9aef26

# The commit actually on disk. A submodule can be bumped without LLAMA_COMMIT
# following it, and the build has to key off what is really there.
LLAMA_SHA := $(shell test -e $(LLAMA_DIR)/.git && git -C $(LLAMA_DIR) rev-parse HEAD 2>/dev/null || echo $(LLAMA_COMMIT))
LLAMA_SHA_SHORT := $(shell echo $(LLAMA_SHA) | cut -c1-12)

# Compiler
CXX      ?= c++
# -DNDEBUG matches the llama.cpp libraries this object links against: they are
# configured with CMAKE_BUILD_TYPE=Release, whose CMAKE_CXX_FLAGS_RELEASE is
# "-O3 -DNDEBUG". Without it the llama.cpp/ggml/common headers inlined into this
# translation unit keep their debug assertions live, and an assert() firing
# inside a NIF takes down the whole VM. Build consistency is the entire reason.
#
# It does NOT disarm GGML_ASSERT. That expands to an unconditional ggml_abort
# (vendor/llama.cpp/ggml/include/ggml.h) and was observed aborting the VM with
# NDEBUG active; only explicit validation at the NIF boundary prevents those.
CXXFLAGS  = -std=c++17 -O2 -DNDEBUG -fPIC -fvisibility=hidden -Wall -Wno-unused-parameter -Wno-unused-function
CXXFLAGS += -I$(ERTS_INCLUDE_DIR)
CXXFLAGS += -I$(FINE_INCLUDE_DIR)
CXXFLAGS += -I$(LLAMA_DIR)/include
CXXFLAGS += -I$(LLAMA_DIR)/ggml/include
CXXFLAGS += -I$(LLAMA_DIR)/common
CXXFLAGS += -I$(LLAMA_DIR)/vendor

# Linker
LDFLAGS = -shared

# Platform detection
UNAME_S := $(shell uname -s)

# Backend selection (auto, metal, cuda, vulkan, cpu)
LLAMA_BACKEND ?= auto

# CMake flags
CMAKE_FLAGS  = -DCMAKE_BUILD_TYPE=Release
CMAKE_FLAGS += -DBUILD_SHARED_LIBS=OFF
CMAKE_FLAGS += -DLLAMA_BUILD_EXAMPLES=OFF
CMAKE_FLAGS += -DLLAMA_BUILD_TESTS=OFF
CMAKE_FLAGS += -DLLAMA_BUILD_SERVER=OFF
CMAKE_FLAGS += -DLLAMA_BUILD_TOOLS=OFF
CMAKE_FLAGS += -DLLAMA_BUILD_APP=OFF
CMAKE_FLAGS += -DLLAMA_OPENSSL=OFF
CMAKE_FLAGS += -DCMAKE_POSITION_INDEPENDENT_CODE=ON

# Backend configuration
ifeq ($(LLAMA_BACKEND),auto)
  ifeq ($(UNAME_S),Darwin)
    CMAKE_FLAGS += -DGGML_METAL=ON -DGGML_METAL_EMBED_LIBRARY=ON
  else
    ifneq ($(shell which nvcc 2>/dev/null),)
      CMAKE_FLAGS += -DGGML_CUDA=ON
    endif
  endif
else ifeq ($(LLAMA_BACKEND),metal)
  CMAKE_FLAGS += -DGGML_METAL=ON -DGGML_METAL_EMBED_LIBRARY=ON
else ifeq ($(LLAMA_BACKEND),cuda)
  CMAKE_FLAGS += -DGGML_CUDA=ON
else ifeq ($(LLAMA_BACKEND),vulkan)
  CMAKE_FLAGS += -DGGML_VULKAN=ON
else ifeq ($(LLAMA_BACKEND),cpu)
  CMAKE_FLAGS += -DGGML_METAL=OFF -DGGML_CUDA=OFF -DGGML_VULKAN=OFF
endif

# Portable builds, for artifacts that leave this machine. ggml defaults
# GGML_NATIVE to ON unless cross-compiling (vendor/llama.cpp/ggml/CMakeLists.txt),
# which adds -march=native (ggml/src/ggml-cpu/CMakeLists.txt) and ties the binary
# to the build machine's CPU. That is free performance for a local build and a
# SIGILL waiting to happen in a published one, so the precompile workflow sets
# LLAMA_PORTABLE=1 while local builds keep -march=native.
ifneq ($(filter 1 true yes,$(LLAMA_PORTABLE)),)
  CMAKE_FLAGS += -DGGML_NATIVE=OFF
  LLAMA_PORTABLE_SUFFIX = -portable
endif

# Custom CMake args
ifdef LLAMA_CMAKE_ARGS
  CMAKE_FLAGS += $(LLAMA_CMAKE_ARGS)
endif

# Build layout. Every key here is load-bearing, because each one selects a
# different cmake configuration or a different set of sources: reusing one build
# tree across them is what made submodule bumps and backend switches silently
# no-op. The directory carries the backend and portability, so a switch gets a
# clean CMakeCache.txt (and switching back is still a cache hit); the stamp
# carries the llama.cpp commit, so a bump forces a rebuild in place.
LLAMA_BUILD = $(BUILD)/llama_build-$(LLAMA_BACKEND)$(LLAMA_PORTABLE_SUFFIX)
LLAMA_STAMP = $(LLAMA_BUILD)/.built-$(LLAMA_SHA_SHORT)

# Platform-specific linker flags
ifeq ($(UNAME_S),Darwin)
  LDFLAGS += -undefined dynamic_lookup
  LDFLAGS += -framework Foundation -framework Accelerate
  ifneq (,$(filter -DGGML_METAL=ON,$(CMAKE_FLAGS)))
    LDFLAGS += -framework Metal -framework MetalKit
  endif
else
  LDFLAGS += -lstdc++ -lm -lpthread
  # ggml-cpu uses OpenMP on Linux when available
  ifneq ($(shell $(CXX) -fopenmp -E - < /dev/null 2>/dev/null && echo yes),)
    LDFLAGS += -lgomp
  endif
endif

# CPU count for parallel builds
NPROC := $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)

# Sources
NIF_SRC = c_src/llama_cpp_ex/llama_nif.cpp
NIF_OBJ = $(BUILD)/llama_nif.o

# Targets
.PHONY: all clean

all: $(NIF_SO)

# Materialize vendor/llama.cpp when it is absent, which is the Hex-tarball case:
# no vendor/ directory and no surrounding git repository. Pinned to LLAMA_COMMIT
# so the tree matches the submodule a git checkout would use. GitHub serves
# arbitrary SHAs, so `init` + `fetch --depth 1 <sha>` gets exactly that commit
# without cloning any history.
$(LLAMA_DIR)/CMakeLists.txt:
	@command -v git >/dev/null 2>&1 || { \
	  echo "error: vendor/llama.cpp is missing and git is not in PATH."; \
	  echo "       Install git, or place a llama.cpp checkout at vendor/llama.cpp."; \
	  exit 1; }
	@if [ -e $(LLAMA_DIR)/.git ]; then \
	  echo "error: $(LLAMA_DIR) exists as a git checkout but has no CMakeLists.txt."; \
	  echo "       In a git checkout the submodule is not fully initialized:"; \
	  echo "         git submodule update --init --recursive"; \
	  echo "       Otherwise a previous clone was interrupted; remove the tree and"; \
	  echo "       rebuild:  rm -rf $(LLAMA_DIR)"; \
	  exit 1; \
	fi
	@echo "==> vendor/llama.cpp not found; cloning $(LLAMA_REPO) at $(LLAMA_COMMIT)"
	rm -rf $(LLAMA_DIR)
	@mkdir -p $(dir $(LLAMA_DIR))
	git -c init.defaultBranch=master init -q $(LLAMA_DIR)
	git -C $(LLAMA_DIR) remote add origin $(LLAMA_REPO)
	git -C $(LLAMA_DIR) fetch --depth 1 --quiet origin $(LLAMA_COMMIT)
	git -C $(LLAMA_DIR) checkout --quiet FETCH_HEAD
	@test -f $@ || { echo "error: clone finished but $@ is still missing"; exit 1; }

# Build llama.cpp static libraries
$(LLAMA_STAMP): $(LLAMA_DIR)/CMakeLists.txt
	@mkdir -p $(LLAMA_BUILD)
	cmake -B $(LLAMA_BUILD) -S $(LLAMA_DIR) $(CMAKE_FLAGS)
	cmake --build $(LLAMA_BUILD) --config Release -j$(NPROC)
	@rm -f $(LLAMA_BUILD)/.built-*
	@touch $@

# Compile NIF
$(NIF_OBJ): $(NIF_SRC) c_src/llama_cpp_ex/llama_nif.h $(LLAMA_STAMP)
	@mkdir -p $(dir $@)
	$(CXX) $(CXXFLAGS) -c $(NIF_SRC) -o $@

# Link NIF - find all static libs from llama.cpp build
$(NIF_SO): $(NIF_OBJ) $(LLAMA_STAMP)
	@mkdir -p $(PREFIX)
	@LIBS=$$(find $(LLAMA_BUILD) -name '*.a' \
		! -path '*/CMakeFiles/*' \
		! -path '*/examples/*' \
		! -path '*/tests/*' \
		| sort); \
	if [ "$(UNAME_S)" = "Linux" ]; then \
		$(CXX) $(NIF_OBJ) -Wl,--start-group $$LIBS -Wl,--end-group $(LDFLAGS) -o $@; \
	else \
		$(CXX) $(NIF_OBJ) $$LIBS $(LDFLAGS) -o $@; \
	fi

clean:
	rm -rf $(BUILD) $(PREFIX)/llama_cpp_ex_nif.so
