# Builds llama.cpp's llama-server (and nothing else) from the vendored
# engine tree and installs it into the gguf_server package, where the Python
# backend spawns it. Modeled on gguf_diffusion's CMakeLists.txt.
#
# The engine source is large and defines dozens of targets; adding it with
# EXCLUDE_FROM_ALL means only what llama-server actually needs gets compiled.
cmake_minimum_required(VERSION 3.15)
project(gguf_server C CXX)

option(GGUF_SERVER_BUILD "Build llama-server and install it alongside the python package" ON)

if(NOT GGUF_SERVER_BUILD)
    return()
endif()

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()

# ── Locate the engine source tree ────────────────────────────────────────────
set(GGUF_SERVER_ENGINE_DIR "" CACHE PATH "Path to the llama.cpp source tree")
if(NOT GGUF_SERVER_ENGINE_DIR AND DEFINED ENV{GGUF_SERVER_ENGINE_DIR})
    set(GGUF_SERVER_ENGINE_DIR "$ENV{GGUF_SERVER_ENGINE_DIR}")
endif()
if(NOT GGUF_SERVER_ENGINE_DIR)
    if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vendor/llama.cpp/CMakeLists.txt")
        set(GGUF_SERVER_ENGINE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/llama.cpp")
    elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../llama.cpp/CMakeLists.txt")
        set(GGUF_SERVER_ENGINE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../llama.cpp")
    endif()
endif()
if(NOT GGUF_SERVER_ENGINE_DIR OR NOT EXISTS "${GGUF_SERVER_ENGINE_DIR}/CMakeLists.txt")
    message(FATAL_ERROR
        "llama.cpp engine source not found. Provide it via one of:\n"
        "  - a vendored copy at vendor/llama.cpp (see scripts/vendor_engine.py)\n"
        "  - a sibling checkout at ../llama.cpp\n"
        "  - -DGGUF_SERVER_ENGINE_DIR=/path/to/llama.cpp or the same env var")
endif()
get_filename_component(GGUF_SERVER_ENGINE_DIR "${GGUF_SERVER_ENGINE_DIR}" ABSOLUTE)
message(STATUS "gguf-server: engine source at ${GGUF_SERVER_ENGINE_DIR}")

# ── Option helper ────────────────────────────────────────────────────────────
# Options are readable from the environment as well as -D, because `pip
# install` makes environment variables far easier to pass than CMake defines:
#   GGUF_SERVER_CUDA=1 pip install .
#   CMAKE_ARGS="-DGGUF_SERVER_CUDA=ON" pip install .
function(gguf_server_option name docstring default)
    if(DEFINED ENV{${name}} AND NOT "$ENV{${name}}" STREQUAL "")
        set(default "$ENV{${name}}")
    endif()
    option(${name} "${docstring}" "${default}")
endfunction()

# ── Windows / MinGW toolchain fixes ──────────────────────────────────────────
if(WIN32 AND NOT MSVC)
    # MinGW-w64's headers pin _WIN32_WINNT to a Windows 7-era value before
    # llama.cpp gets a chance to set it (common/common.h and ggml/include/ggml.h
    # both only define it "if not already defined", so their 0x0A00 never
    # applies). cpp-httplib then hard-errors with "doesn't support Windows 8 or
    # lower" and ::CreateFile2 goes undeclared, which takes down httplib.cpp,
    # download.cpp and hf-cache.cpp. Set it up front, matching what the engine
    # expects everywhere else.
    add_compile_definitions(_WIN32_WINNT=0x0A00 WINVER=0x0A00 NTDDI_VERSION=0x0A000000)

    # A MinGW-built binary otherwise needs libstdc++-6.dll, libgcc_s_seh-1.dll
    # and libwinpthread-1.dll from the toolchain at run time. The wheel ships a
    # single file and Python spawns it without MSYS2 on PATH, so link them in.
    gguf_server_option(GGUF_SERVER_MINGW_STATIC
        "Statically link the MinGW runtime into llama-server" ON)
    if(GGUF_SERVER_MINGW_STATIC)
        add_link_options(-static-libgcc -static-libstdc++ -static)

        # ggml uses OpenMP, and CMake's FindOpenMP resolves it to
        # libgomp.dll.a — an explicit path to an *import* library, which
        # -static cannot override, leaving the binary needing libgomp-1.dll.
        # Point OpenMP at the static archive instead; if there isn't one,
        # drop OpenMP rather than ship a half-static binary (ggml falls back
        # to its own thread pool).
        if(NOT OpenMP_gomp_LIBRARY)
            set(_gguf_saved_suffixes ${CMAKE_FIND_LIBRARY_SUFFIXES})
            set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
            find_library(GGUF_SERVER_LIBGOMP_STATIC NAMES gomp)
            set(CMAKE_FIND_LIBRARY_SUFFIXES ${_gguf_saved_suffixes})
            if(GGUF_SERVER_LIBGOMP_STATIC)
                message(STATUS "gguf-server: static OpenMP runtime ${GGUF_SERVER_LIBGOMP_STATIC}")
                set(OpenMP_gomp_LIBRARY "${GGUF_SERVER_LIBGOMP_STATIC}"
                    CACHE FILEPATH "gguf-server: static OpenMP runtime" FORCE)
            else()
                message(STATUS "gguf-server: no static libgomp found, building without OpenMP")
                set(GGML_OPENMP OFF CACHE BOOL "gguf-server: keep the binary self-contained" FORCE)
            endif()
        endif()
    endif()
endif()

# ── Trim the engine build down to llama-server ───────────────────────────────
# Static libs (ggml, llama, common, mtmd, …) linked straight into the
# executable keep the wheel to a single binary with no rpath/DLL handling.
set(BUILD_SHARED_LIBS      OFF CACHE BOOL "gguf-server: static engine for a single-file install" FORCE)
set(LLAMA_BUILD_COMMON     ON  CACHE BOOL "gguf-server: llama-server needs llama-common" FORCE)
set(LLAMA_BUILD_TOOLS      ON  CACHE BOOL "gguf-server: llama-server lives under tools/" FORCE)
set(LLAMA_BUILD_SERVER     ON  CACHE BOOL "gguf-server: the server is the bundled artifact" FORCE)
set(LLAMA_BUILD_TESTS      OFF CACHE BOOL "gguf-server: no tests" FORCE)
set(LLAMA_BUILD_EXAMPLES   OFF CACHE BOOL "gguf-server: no examples" FORCE)
set(LLAMA_BUILD_APP        OFF CACHE BOOL "gguf-server: no unified binary" FORCE)
set(LLAMA_TOOLS_INSTALL    OFF CACHE BOOL "gguf-server: install rules are ours" FORCE)
set(LLAMA_TESTS_INSTALL    OFF CACHE BOOL "gguf-server: install rules are ours" FORCE)

# llama-server embeds a web UI. Building it from source needs npm and a full
# `npm install`, which is far too heavy for `pip install`; the prebuilt bundle
# is fetched from Hugging Face instead when the network allows. Neither is
# required — without assets the server still runs and only its browser UI at
# http://host:port/ is missing (this package ships its own GUI regardless).
gguf_server_option(GGUF_SERVER_BUILD_UI
    "Build llama-server's embedded web UI from source with npm" OFF)
set(LLAMA_BUILD_UI ${GGUF_SERVER_BUILD_UI} CACHE BOOL "gguf-server: npm UI build is opt-in" FORCE)
set(LLAMA_USE_PREBUILT_UI ON CACHE BOOL "gguf-server: fetch the prebuilt web UI when available" FORCE)

# ── HTTPS / OpenSSL ──────────────────────────────────────────────────────────
# Off by default. llama-server only needs OpenSSL to *download* models over
# HTTPS (-hf / URL arguments); this package always hands it local file paths,
# so the dependency buys nothing and costs portability: llama.cpp's
# find_package(OpenSSL) happily picks up a foreign-ABI install — an MSYS2 /
# MinGW libcrypto next to an MSVC toolchain puts MinGW headers on cl.exe's
# include path and breaks httplib.cpp, download.cpp and hf-cache.cpp with
# "winnt.h: fatal error C1189: No supported target architecture".
gguf_server_option(GGUF_SERVER_OPENSSL
    "Link llama-server against OpenSSL for HTTPS model downloads" OFF)
set(LLAMA_OPENSSL ${GGUF_SERVER_OPENSSL} CACHE BOOL "gguf-server: HTTPS support is opt-in" FORCE)

# ── GPU / accelerator backends ───────────────────────────────────────────────
# Each maps to the ggml option of the same name and is off unless requested.
# Anything not listed here can still be passed straight through as -DGGML_*.
set(GGUF_SERVER_BACKEND_NAMES
    CUDA        # NVIDIA, needs the CUDA toolkit
    HIP         # AMD, needs ROCm/HIP
    METAL       # Apple; on by default on macOS, with the shader library embedded
    VULKAN      # cross-vendor, needs the Vulkan SDK
    SYCL        # Intel oneAPI
    OPENCL      # Adreno and other OpenCL devices
    MUSA        # Moore Threads
    WEBGPU
    ZDNN        # IBM Z
    OPENVINO
    HEXAGON     # Qualcomm DSP
    BLAS        # CPU BLAS acceleration
    RPC         # offload to a remote ggml backend
)
set(GGUF_SERVER_ENABLED_BACKENDS "")
foreach(backend IN LISTS GGUF_SERVER_BACKEND_NAMES)
    gguf_server_option(GGUF_SERVER_${backend}
        "Build llama-server with the ggml ${backend} backend" OFF)
    if(GGUF_SERVER_${backend})
        set(GGML_${backend} ON CACHE BOOL "gguf-server: ${backend} backend requested" FORCE)
        list(APPEND GGUF_SERVER_ENABLED_BACKENDS ${backend})
    endif()
endforeach()

if(GGUF_SERVER_ENABLED_BACKENDS)
    string(REPLACE ";" ", " _backend_list "${GGUF_SERVER_ENABLED_BACKENDS}")
    message(STATUS "gguf-server: accelerator backends requested: ${_backend_list}")
elseif(APPLE)
    message(STATUS "gguf-server: no backend requested; Metal is enabled by default on macOS")
else()
    message(STATUS "gguf-server: CPU-only build (enable e.g. -DGGUF_SERVER_CUDA=ON for GPU)")
endif()

# EXCLUDE_FROM_ALL drops the engine's own install() rules (headers, libs,
# pkg-config) that would otherwise land in the wheel, and keeps every target
# it defines out of the default build — the dependency below pulls in
# llama-server and just the libraries it links against.
add_subdirectory("${GGUF_SERVER_ENGINE_DIR}" "${CMAKE_BINARY_DIR}/llama-engine" EXCLUDE_FROM_ALL)

if(NOT TARGET llama-server)
    message(FATAL_ERROR "engine did not define the llama-server target — is ${GGUF_SERVER_ENGINE_DIR} the llama.cpp source tree?")
endif()

add_custom_target(gguf_server_engine ALL)
add_dependencies(gguf_server_engine llama-server)

# Install into the wheel (SKBUILD_PLATLIB_DIR) and also into the source tree
# so editable installs (`pip install -e .`) find the binary — the same dual
# install gguf_diffusion and gguf_editor use.
install(
    PROGRAMS $<TARGET_FILE:llama-server>
    DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/src/gguf_server/bin"
)
if(DEFINED SKBUILD_PLATLIB_DIR)
    install(
        PROGRAMS $<TARGET_FILE:llama-server>
        DESTINATION "${SKBUILD_PLATLIB_DIR}/gguf_server/bin"
    )
endif()
