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

EAPI=8

inherit cmake multilib

# The web app (the browser UI served by lemond) is bundled with webpack, and
# upstream drives that through `npm ci` at build time.  That needs the network,
# so the dependency tree is shipped as a separate distfile generated from this
# release's src/web-app/package-lock.json with:
#   npm ci --ignore-scripts --no-audit --no-fund
#   tar caf lemonade-webapp-node_modules-${WEBAPP_NODE_MODULES_PV}.tar.xz node_modules
# The bundle is only regenerated when src/web-app/package-lock.json changes, so
# it is pinned to the release it was built from instead of following ${PV}.
# 11.5.1 ships the exact same lockfile as 11.5.0.
WEBAPP_NODE_MODULES_PV="11.5.0"
WEBAPP_NODE_MODULES="${PN}-webapp-node_modules-${WEBAPP_NODE_MODULES_PV}.tar.xz"

DESCRIPTION="Local LLM server with GPU and NPU acceleration"
HOMEPAGE="https://lemonade-server.ai/ https://github.com/lemonade-sdk/lemonade"
SRC_URI="
	https://github.com/lemonade-sdk/lemonade/archive/refs/tags/v${PV}.tar.gz
		-> ${P}.tar.gz
	webapp? ( https://distfiles.obentoo.org/${WEBAPP_NODE_MODULES} )
"

# The webapp bundle links the JavaScript dependency tree into
# renderer.bundle.js: React and KaTeX (MIT), highlight.js (BSD-3-Clause) and
# a handful of Apache-2.0 pieces, as listed in the .LICENSE.txt webpack emits
# next to the bundle.
LICENSE="Apache-2.0 webapp? ( BSD MIT )"
SLOT="0"
KEYWORDS="~amd64 ~arm64"
IUSE="+caps +systemd test +webapp"
RESTRICT="!test? ( test )"

# Everything upstream cannot find on the system is pulled in with FetchContent
# at configure time, which a sandboxed build cannot do, so all of these are
# hard dependencies rather than optional ones.  libdrm is not optional either:
# the Linux branch links drm_amdgpu unconditionally to read AMD GPU memory.
COMMON_DEPEND="
	>=app-arch/zstd-1.5.5:=
	>=dev-cpp/cpp-httplib-0.26.0:=
	>=net-libs/libwebsockets-4.3.3:=
	>=net-misc/curl-8.5.0:=
	net-libs/mbedtls:3=
	x11-libs/libdrm[video_cards_amdgpu]
	caps? ( sys-libs/libcap )
	systemd? ( sys-apps/systemd:= )
"
# cli11 and nlohmann_json are header-only.
DEPEND="
	${COMMON_DEPEND}
	>=dev-cpp/cli11-2.4.2
	>=dev-cpp/nlohmann_json-3.11.3
"
RDEPEND="
	${COMMON_DEPEND}
	acct-group/lemonade
	acct-user/lemonade
	app-arch/unzip
	!!sci-ml/lemonade-bin
"
BDEPEND="
	virtual/pkgconfig
	webapp? ( net-libs/nodejs[npm] )
"

PATCHES=(
	# src/web-app/BuildWebApp.cmake is unchanged since 11.5.0, so the patch is
	# shared rather than duplicated under a new name.
	"${FILESDIR}"/${PN}-11.5.0-webapp-prebuilt-node-modules.patch
)

src_unpack() {
	unpack "${P}.tar.gz"
	use webapp && unpack "${WEBAPP_NODE_MODULES}"
}

# Rewrite one upstream CMake line, asserting the match first.  sed exits 0
# when its pattern matches nothing, so a bare `sed ... || die` cannot tell
# "rewritten" from "upstream moved the line".  That is not hypothetical here:
# 11.5.2 moved the mbedtls probe from src/cpp/cli/CMakeLists.txt into the
# top-level CMakeLists.txt, the rewrite below kept editing the old file, and
# the package failed at configure time on every release from 11.5.2 to 11.8.1
# without anything in the ebuild noticing.  Break loudly on the next move.
lemonade_sed_cmake() {
	local pattern=${1} replacement=${2} file=${3}

	grep -qF "${pattern}" "${file}" ||
		die "upstream no longer has \"${pattern}\" in ${file}; recheck the probe"
	sed -i -e "s|${pattern}|${replacement}|" "${file}" || die
}

src_prepare() {
	cmake_src_prepare

	# libsystemd (journald log sink) and libcap (capability inheritance for the
	# backend processes lemond spawns) are probed with bare pkg_check_modules()
	# calls that have no option() to turn them off, so the detection itself has
	# to be neutralised or the build links them behind the USE flag's back.
	if ! use systemd; then
		lemonade_sed_cmake \
			'pkg_check_modules(SYSTEMD QUIET libsystemd)' \
			'set(SYSTEMD_FOUND FALSE)' \
			CMakeLists.txt
	fi
	if ! use caps; then
		lemonade_sed_cmake \
			'pkg_check_modules(LIBCAP QUIET libcap)' \
			'set(LIBCAP_FOUND FALSE)' \
			CMakeLists.txt
	fi

	# mbedtls (HTTPS, and the SHA-256/SHA-1 digest check lemond runs over every
	# model it downloads) is looked up under its upstream names, but Gentoo
	# suffixes the slot onto everything it installs: mbedtls-3.pc /
	# libmbedtls-3.so, headers under /usr/include/mbedtls3.  Point pkg-config at
	# the slotted module names.  Keep in sync with the slot in COMMON_DEPEND.
	lemonade_sed_cmake \
		'pkg_check_modules(MBEDTLS QUIET mbedtls mbedx509 mbedcrypto)' \
		'pkg_check_modules(MBEDTLS QUIET mbedtls-3 mbedx509-3 mbedcrypto-3)' \
		CMakeLists.txt

	# Close the hand-rolled fallback the probe drops into when pkg-config comes
	# up empty.  It searches for <mbedtls/ssl.h> and libmbedtls.so, and the :3
	# slot installs neither, so it can only ever match net-libs/mbedtls:0 -- the
	# 2.28 series, which upstream's BRANCHES.md no longer lists as a supported
	# LTS and which this package does not depend on.  Nothing downstream catches
	# that: the CLI only calls mbedtls_md_*, which is ABI-stable across 2.28 and
	# 3.6, so the mismatch compiles, links, and ships a :3= subslot guarding a
	# library the binary never loads.  Make the configure fail instead.
	lemonade_sed_cmake \
		'find_library(MBEDTLS_LIBRARY NAMES mbedtls)' \
		'set(MBEDTLS_LIBRARY MBEDTLS_LIBRARY-NOTFOUND)' \
		CMakeLists.txt

	# dev-cpp/cpp-httplib ships a CMake package config (httplib::httplib) but no
	# pkg-config file, while upstream only probes httplib through
	# pkg_search_module().  With no .pc the probe fails, USE_SYSTEM_HTTPLIB flips
	# to OFF and the build tries to FetchContent httplib, which the sandbox
	# cannot reach.  Synthesize the .pc the probe looks for (see the
	# PKG_CONFIG_PATH export in src_configure).  No feature macros in Cflags on
	# purpose: upstream's system branch skips them too, because they would turn
	# on header features needing link deps the interface target does not carry.
	local httplib_ver
	httplib_ver=$(sed -nE 's/.*CPPHTTPLIB_VERSION[[:space:]]+"([0-9.]+)".*/\1/p' \
		"${ESYSROOT}"/usr/include/httplib.h) || die
	[[ -n ${httplib_ver} ]] || die "could not read cpp-httplib version"
	mkdir -p "${T}"/pkgconfig || die
	cat > "${T}"/pkgconfig/httplib.pc <<-EOF || die
		prefix=${ESYSROOT}/usr
		libdir=\${prefix}/$(get_libdir)
		includedir=\${prefix}/include
		Name: httplib
		Description: cpp-httplib (Gentoo, synthesized by the ebuild)
		Version: ${httplib_ver}
		Cflags: -I\${includedir}
		Libs: -L\${libdir} -lcpp-httplib
	EOF
}

src_configure() {
	# Let pkg_search_module() find the httplib.pc synthesized in src_prepare.
	local -x PKG_CONFIG_PATH="${T}/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}"

	local mycmakeargs=(
		-DBUILD_TESTING=$(usex test)
		-DBUILD_WEB_APP=$(usex webapp)
		# The Tauri desktop shell and the AppIndicator tray are separate
		# products that would each need their own npm/cargo dependency set.
		-DBUILD_TAURI_APP=OFF
		-DREQUIRE_LINUX_TRAY=OFF
		# The web app bundles its own KaTeX from the node_modules distfile;
		# the system-modules path expects Debian's /usr/share/nodejs layout.
		-DUSE_SYSTEM_NODEJS_MODULES=OFF
		# Turn any dependency upstream failed to find into a configure-time
		# error instead of a silent download attempt.
		-DFETCHCONTENT_FULLY_DISCONNECTED=ON
	)

	cmake_src_configure
}

src_compile() {
	if use webapp; then
		# Consumed by the patch in src/web-app/BuildWebApp.cmake.
		local -x LEMONADE_PREBUILT_NODE_MODULES="${WORKDIR}/node_modules"
		# `npm run build` only spawns the bundled webpack, but keep npm from
		# touching the network or ${HOME} regardless.
		local -x npm_config_cache="${T}/npm-cache"
		local -x npm_config_offline=true
		local -x npm_config_audit=false
		local -x npm_config_fund=false
		local -x npm_config_update_notifier=false
	fi

	cmake_src_compile
}

src_install() {
	cmake_src_install

	# The service account comes from acct-user/lemonade and
	# acct-group/lemonade, so the sysusers.d drop-in only duplicates them.
	rm -r "${ED}"/usr/lib/sysusers.d || die

	newinitd "${FILESDIR}"/lemond.initd lemond
	newconfd "${FILESDIR}"/lemond.confd lemond

	# 0640, not the 0644 newconfd leaves behind. This file carries HF_TOKEN,
	# LEMONADE_API_KEY and LEMONADE_ADMIN_API_KEY. root:root rather than
	# root:lemonade is deliberate and sufficient: OpenRC sources conf.d as root
	# and only then drops to the lemonade user, so the daemon never reads the
	# file, and naming the group would need the account on the BUILD host.
	fperms 0640 /etc/conf.d/lemond

	# /var/lib/lemonade is created at first start, not here: the lemonade
	# service account does not exist on the build host, so fowners by name would
	# fail.  systemd's StateDirectory= and the initd's checkpath both create it
	# owned by lemonade:lemonade on startup.
}

pkg_postinst() {
	elog "lemond stores models and its config.json under /var/lib/lemonade."
	elog "Start it with one of:"
	elog "    rc-service lemond start"
	elog "    systemctl start lemond.service"
	elog
	elog "Inference backends (llama.cpp, whisper.cpp, vLLM, ONNX Runtime,"
	elog "Ryzen AI/NPU, ...) are not built here: lemond downloads and manages"
	elog "them at runtime, which is why app-arch/unzip is a runtime dependency."
	elog
	elog "Set LEMONADE_API_KEY and HF_TOKEN in /etc/conf.d/lemond for the"
	elog "OpenRC service, or in /etc/default/lemond for the systemd unit --"
	elog "that is the path upstream's unit actually reads (EnvironmentFile=)."
	elog "Both should be mode 0640; the OpenRC one is installed that way."
	elog
	elog "DISCLOSURE: lemond announces itself over UDP for server discovery,"
	elog "and that is ON BY DEFAULT. It broadcasts on RFC1918 interfaces even"
	elog "when the API itself is bound to loopback, so a machine on the same"
	elog "LAN can see that this host runs lemonade. Turn it off with"
	elog "--no-broadcast in LEMOND_OPTS, or \"broadcast\": false in"
	elog "/var/lib/lemonade/config.json."
}