# Copyright 2026 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

EAPI=8

ROCM_VERSION="6.3"
PYTHON_COMPAT=( python3_{12..14} )

inherit python-single-r1 rocm toolchain-funcs wrapper

DESCRIPTION="All-in-one local AI server (LLM, image, speech, TTS) on ggml/llama.cpp"
HOMEPAGE="https://github.com/LostRuins/koboldcpp"
SRC_URI="https://github.com/LostRuins/${PN}/archive/refs/tags/v${PV}.tar.gz -> ${P}.gh.tar.gz"
S="${WORKDIR}/${PN}-${PV}"

# koboldcpp + KoboldAI Lite UI are AGPL-3.0; the vendored ggml/llama.cpp/
# stable-diffusion.cpp/TTS.cpp code is MIT per
# MIT_LICENSE_GGML_SDCPP_LLAMACPP_ONLY.md.
LICENSE="AGPL-3+ MIT"
SLOT="0"
KEYWORDS="~amd64"

# Vulkan on by default (upstream's supported GPU path for AMD+NVIDIA).
# ROCm ships as upstream target koboldcpp_hipblas, untested on the
# maintainer's hardware.  No CUDA: nothing to build-test it with.
IUSE="+vulkan rocm"

# LLAMA_NOAVX1/LLAMA_NOAVX2 pick one of the Makefile's three x86 flag tiers
# (lines 172-191 of the 1.122.1 Makefile):
#   unset            -mavx2 -mavx -mfma -mf16c -msse3 -mssse3
#   LLAMA_NOAVX2=1   -mavx -msse3 -mssse3
#   LLAMA_NOAVX1=1   -msse3 -mssse3
# f16c/fma3 exist only inside the AVX2 tier, hence the REQUIRED_USE coupling.
IUSE+=" cpu_flags_x86_avx cpu_flags_x86_avx2 cpu_flags_x86_f16c cpu_flags_x86_fma3"

REQUIRED_USE="
	${PYTHON_REQUIRED_USE}
	rocm? ( ${ROCM_REQUIRED_USE} )
	cpu_flags_x86_avx2? ( cpu_flags_x86_avx cpu_flags_x86_f16c cpu_flags_x86_fma3 )
"

# No third-party Python deps: koboldcpp.py and kcpp_agent.py import stdlib
# only (checked 2026-10-13).  requirements.txt mentions jinja2, but that is
# for the model-conversion scripts under conversion/ and gguf-py/, which are
# not installed here.
RDEPEND="
	${PYTHON_DEPS}
	rocm? (
		>=dev-util/hip-${ROCM_VERSION}
		>=sci-libs/hipBLAS-${ROCM_VERSION}
	)
	vulkan? ( media-libs/vulkan-loader )
"
DEPEND="
	${RDEPEND}
	vulkan? ( dev-util/vulkan-headers )
"
BDEPEND="
	${PYTHON_DEPS}
	vulkan? ( media-libs/shaderc )
"

# Prebuilt binaries shipped in the tarball (Windows EXEs/DLLs, a stray
# x86-64 ELF, bundled glslc).  All seven confirmed present in v1.122.1.
# Die if any is missing: with a Manifest-pinned distfile that can only mean
# this list was never re-checked for the version being built.
KCPP_BLOBS=(
	aria2c-win.exe
	cudart64_110.dll
	cudart64_12.dll
	glslc-linux
	glslc.exe
	simplecpuinfo
	simplecpuinfo.exe
)

pkg_setup() {
	python-single-r1_pkg_setup
}

src_prepare() {
	default

	# Release flags hardcode -s (Makefile lines 62-63); drop it so Portage
	# keeps control of stripping.  -DNDEBUG stays.
	grep -q -- '-DNDEBUG -s' Makefile ||
		die "strip-deferral anchor '-DNDEBUG -s' is gone from Makefile"
	sed -i -e 's/-DNDEBUG -s/-DNDEBUG/g' Makefile || die

	# Removing the bundled glslc binaries forces the system shaderc one.
	# The Makefile tests LLAMA_USE_BUNDLED_GLSLC with [ -n ] (lines
	# 831/875) and defaults it to 1 (line 101), so src_compile overrides it
	# with an EMPTY value -- 0 would truthy-match the test.
	local blob
	for blob in "${KCPP_BLOBS[@]}"; do
		[[ -f ${blob} ]] || die "expected prebuilt blob ${blob} not found; re-audit the tarball"
	done
	rm -f "${KCPP_BLOBS[@]}" || die
}

src_compile() {
	tc-export CC CXX

	# LLAMA_PORTABLE=1 suppresses the Makefile's -march=native (x86,
	# lines 187-190) and -mcpu=native (aarch64, line 371) additions so the
	# result is not tuned to the build host.
	#
	# Upstream overwrites CFLAGS/CXXFLAGS/LDFLAGS outright (its include
	# paths live in those variables), so user *FLAGS cannot be passed
	# through make; ISA choice happens via cpu_flags_x86_* below.
	local makeargs=(
		LLAMA_PORTABLE=1
		LLAMA_USE_BUNDLED_GLSLC=
	)

	if use amd64; then
		if use cpu_flags_x86_avx2; then
			: # top tier, nothing to set
		elif use cpu_flags_x86_avx; then
			makeargs+=( LLAMA_NOAVX2=1 )
		else
			makeargs+=( LLAMA_NOAVX1=1 )
		fi
	fi

	local targets=( koboldcpp_default )
	if use vulkan; then
		targets+=( koboldcpp_vulkan )
		makeargs+=( LLAMA_VULKAN=1 )
	fi

	emake "${makeargs[@]}" "${targets[@]}"

	if use rocm; then
		# Built in a second emake call so it reuses the objects from the
		# first run, matching upstream's default target structure.
		#
		# The Makefile locates its HIP toolchain via $(wildcard /opt/rocm)
		# and appends $(shell amdgpu-arch) (the build host's GPU) to
		# GPU_TARGETS; point all three at the Gentoo hip installation and
		# the AMDGPU_TARGETS USE_EXPAND instead.
		rocm_add_sandbox
		emake "${makeargs[@]}" \
			LLAMA_HIPBLAS=1 \
			ROCM_PATH="$(hipconfig -R)" \
			HCC="$(hipconfig -l)/clang" \
			HCXX="$(hipconfig -l)/clang++" \
			GPU_TARGETS="${AMDGPU_TARGETS}" \
			koboldcpp_hipblas
	fi
}

src_install() {
	local dest="/usr/share/${PN}"

	insinto "${dest}"
	# kcpp_agent.py has to sit next to koboldcpp.py: the launcher builds
	# the path <script_dir>/kcpp_agent.py and does a plain "import
	# kcpp_agent" for --agent (lines 11713/11723).
	doins koboldcpp.py kcpp_agent.py
	# Backend libraries are discovered relative to the launcher's own path.
	doins koboldcpp_*.so
	# Web UI, API docs, SD/TTS assets, chat-format adapters.
	doins -r embd_res kcpp_adapters

	python_fix_shebang "${ED}${dest}"/{koboldcpp,kcpp_agent}.py

	make_wrapper "${PN}" "${EPYTHON} ${EPREFIX}${dest}/koboldcpp.py"

	dodoc README.md
}

pkg_postinst() {
	elog "koboldcpp installed to ${EROOT}/usr/share/${PN}; run it with:"
	elog "    koboldcpp --model /path/to/model.gguf"
	elog
	if use vulkan; then
		elog "Vulkan (the official AMD/NVIDIA GPU path) is available; add"
		elog "    --usevulkan"
		elog "to offload to the GPU."
	fi
	if use rocm; then
		elog "ROCm/hipBLAS is available; add"
		elog "    --usecublas"
		elog "to offload to an AMD GPU (koboldcpp reuses the cuBLAS flag"
		elog "for the hipBLAS backend)."
		elog
		elog "Note: USE=rocm is wired but was not build-verified by the"
		elog "maintainer; please report the result of your build."
	fi
	elog "Models are NOT bundled; download a .gguf yourself."
}