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

EAPI=8

inherit go-env go-module

# Three distfiles below are generated by bentoo and hosted on
# distfiles.obentoo.org. They carry three independent versions because they
# have three different triggers -- pinning them all to ${PV} would mean
# republishing 45 MB of unchanged node_modules on every LocalAI release.
#
# 1. ${P}-vendor.tar.xz -- REGENERATE ON EVERY BUMP (go.mod moves with the
#    release). pkg/grpc/proto is generated rather than shipped, and
#    backend/go/* import it, so `go mod vendor` FAILS until protoc has run:
#
#      tar xzf LocalAI-v${PV}-source.tar.gz -C src && cd src
#      mkdir -p pkg/grpc/proto
#      protoc --experimental_allow_proto3_optional -Ibackend/ \
#        --go_out=pkg/grpc/proto/ --go_opt=paths=source_relative \
#        --go-grpc_out=pkg/grpc/proto/ --go-grpc_opt=paths=source_relative \
#        backend/backend.proto
#      go mod vendor
#      tar caf ${P}-vendor.tar.xz vendor
#
#    Do NOT reach for `go get -tool` to pull the protoc plugins into the
#    module graph: it downgrades grpc 1.80.0 -> 1.69.0-dev, protobuf
#    1.36.11 -> 1.34.2 and libp2p 0.48.0 -> 0.36.5, and drops cogito and nib
#    outright. The tree would then be built against dependencies upstream
#    never tested.
#
# 2. ${PN}-react-ui-node_modules-${NODE_MODULES_PV}.tar.xz -- REGENERATE WHEN
#    core/http/react-ui/package-lock.json CHANGES, not every bump:
#
#      cd core/http/react-ui
#      npm ci --ignore-scripts        # --ignore-scripts: @playwright/test's
#                                     # postinstall would fetch browsers
#      # vite resolves its native bits through per-platform optional deps, so
#      # a tree built here is x86_64-only. Overlay the arm64 ones so one
#      # distfile serves both KEYWORDS (costs 12 MB):
#      npm ci --ignore-scripts --cpu=arm64 --os=linux   # in a scratch dir
#      cp -a scratch/node_modules/{lightningcss-linux-arm64-*} node_modules/
#      cp -a scratch/node_modules/@rolldown/binding-linux-arm64-* \
#            node_modules/@rolldown/
#      tar caf ${PN}-react-ui-node_modules-${PV}.tar.xz node_modules
#
#    Upstream's Makefile builds this with `npm install`; `npm ci` is used
#    instead so the lockfile is honoured rather than re-resolved.
#
# 3. protoc-gen-go-grpc-${PGGG_PV}-vendor.tar.xz -- REGENERATE WHEN UPSTREAM
#    REPINS THE PLUGIN (Makefile, install-go-tools). Only the gRPC generator
#    is bundled; protoc-gen-go comes from LocalAI's own vendor tree instead.
#    See src_compile.
#
#      mkdir protoc-gen-go-grpc && cd protoc-gen-go-grpc
#      go mod init bentoo.local/protoc-gen-go-grpc-bundle
#      go get google.golang.org/grpc/cmd/protoc-gen-go-grpc@v${PGGG_PV}
#      go mod edit -tool=google.golang.org/grpc/cmd/protoc-gen-go-grpc
#      go mod tidy && go mod vendor && cd ..
#      tar caf protoc-gen-go-grpc-${PGGG_PV}-vendor.tar.xz protoc-gen-go-grpc

# Upstream pins protoc-gen-go-grpc by commit SHA
# (1958fcbe2ca8bd93af633f11e97d44e567e945af); that SHA is release v1.4.0.
PGGG_PV="1.4.0"
# Bump only when core/http/react-ui/package-lock.json actually changes.
NODE_MODULES_PV="4.9.0"

MY_P="LocalAI-v${PV}"

DESCRIPTION="Self-hosted OpenAI-compatible inference API with a model gallery"
HOMEPAGE="https://localai.io/ https://github.com/mudler/LocalAI"
SRC_URI="
	https://github.com/mudler/LocalAI/releases/download/v${PV}/${MY_P}-source.tar.gz
	https://distfiles.obentoo.org/${P}-vendor.tar.xz
	https://distfiles.obentoo.org/${PN}-react-ui-node_modules-${NODE_MODULES_PV}.tar.xz
	https://distfiles.obentoo.org/protoc-gen-go-grpc-${PGGG_PV}-vendor.tar.xz
"
# The curated release tarball has NO top-level directory: it extracts straight
# into backend/ cmd/ core/ ... Getting this wrong produces a confusing
# "file not found" much later, in src_compile.
S="${WORKDIR}"

# MIT is LocalAI's own. The rest are the licences actually present in the
# vendored tree: Apache-2.0 (107 modules), MIT (151), BSD-3 (12), MPL-2.0 (6),
# ISC (2), BSD-2 (1).
LICENSE="MIT Apache-2.0 BSD BSD-2 ISC MPL-2.0"
SLOT="0"
KEYWORDS="~amd64 ~arm64"

# No acceleration USE flags, and that is deliberate rather than an omission:
# LocalAI resolves its inference backends as OCI artifacts at RUNTIME, so no
# backend is ever compiled in and a CPU-only install is the default. The
# acceleration matrix is optional by construction. IUSE is therefore not set
# at all -- an empty IUSE="" is a QA error, and the absence is the statement.

# The build reaches no network at all -- verified by building inside a network
# namespace with every interface down. Three upstream fetches are removed:
# the prebuilt protoc download (dev-libs/protobuf instead), install-go-tools'
# two `go install ...@ver` (see src_compile), and the react-ui `npm install`
# (the node_modules distfile). Hence no RESTRICT="network-sandbox".
# The test suite does download models, so it stays restricted.
PROPERTIES="test_network"
RESTRICT="test strip"

# No dev-go/protobuf-go here on purpose -- see src_compile: protoc-gen-go is
# built from LocalAI's own vendor tree so that the generator and the protobuf
# runtime are the same version.
# pkgcheck reports NonsolvableDepsInDev for the x32 dev profile
# (default/linux/amd64/23.0/x32) against the nodejs dependency. That is not a
# defect here and cannot be fixed here: net-libs/nodejs carries
# "~amd64 ~arm ~arm64 ~loong ~ppc64 ~riscv ~x86 ~x64-macos" and no x32 keyword
# at all, so every package that needs npm reports the same. Narrowing KEYWORDS
# to silence it would drop arches where this builds fine.
BDEPEND="
	>=dev-lang/go-1.26.0
	dev-libs/protobuf
	>=net-libs/nodejs-24:*[npm]
"

# The service account only. Nothing else is linked in: CGO_ENABLED=0 makes the
# binary static, and the inference backends are OCI artifacts fetched at
# runtime rather than libraries resolved at build time.
RDEPEND="
	acct-group/localai
	acct-user/localai
"

# NOTE: src_unpack is deliberately NOT defined. go-module.eclass exports it,
# and go-module_src_unpack ends by calling go-env_set_compile_environment;
# a local `src_unpack() { default; }` would override the eclass and drop that
# call silently. The eclass's own version does the right thing here: it
# unpacks everything and then skips `ego mod verify` because ${S}/vendor
# exists. The three bentoo distfiles were packed with distinct top-level
# entries (vendor/, node_modules/, protoc-gen-go-grpc/) precisely so that
# none collides with the source tarball's own go.mod, go.sum or vendor/ when
# they all land in ${WORKDIR}.

src_prepare() {
	# vite resolves dependencies relative to the project directory, so the
	# staged tree has to sit next to package.json rather than at ${WORKDIR}.
	mv "${WORKDIR}"/node_modules core/http/react-ui/ || die

	# core/config/inference_defaults.json ships in the tarball, so the
	# generator that would fetch it from unsloth is dead weight -- but a
	# `go generate` would still run it and reach the network. Drop it.
	# The grep guard matters: sed exits 0 when it matches nothing, so without
	# it an upstream rename would silently restore the fetch.
	grep -q '^//go:generate go run \./gen_inference_defaults/' \
		core/config/inference_defaults.go \
		|| die "the go:generate anchor moved; recheck core/config/inference_defaults.go"
	sed -i '/^\/\/go:generate/d' core/config/inference_defaults.go || die

	# .gitmodules names docs/themes/hugo-theme-relearn and
	# backend/rust/kokoros/sources/Kokoros. The curated tarball ships both as
	# EMPTY directories, so there is nothing to strip and nothing that can
	# reach the network -- asserted rather than assumed, because a future
	# tarball that did embed them would change the licence set.
	local sub
	for sub in docs/themes/hugo-theme-relearn backend/rust/kokoros/sources/Kokoros; do
		if [[ -n $(find "${sub}" -mindepth 1 -print -quit 2>/dev/null) ]]; then
			die "${sub} is no longer empty; re-check the submodule handling"
		fi
	done

	default
}

src_compile() {
	# protoc resolves its code generators through PATH, so both must exist as
	# executables before it runs.
	#
	# protoc-gen-go is built from LocalAI's OWN vendor tree, which beats both
	# alternatives: it is v1.36.11, the very version of the protobuf runtime
	# the binary links against. Upstream's Makefile pins the generator at
	# v1.34.2 and dev-go/protobuf-go follows ::gentoo; either can drift AHEAD
	# of the vendored runtime, and generated code then demands a runtime newer
	# than the one vendored -- a failure that reads like an upstream bug.
	# Asserted rather than assumed: `go mod vendor` includes this package
	# today, and a bump where it stops being included must fail here, loudly.
	[[ -d vendor/google.golang.org/protobuf/cmd/protoc-gen-go ]] ||
		die "protoc-gen-go left the vendor tree; regenerate the vendor distfile or move the plugin into the bundle"
	mkdir -p "${T}"/bin || die
	ego build -o "${T}"/bin/protoc-gen-go \
		google.golang.org/protobuf/cmd/protoc-gen-go

	# protoc-gen-go-grpc, by contrast, has no Gentoo package AND is not
	# reachable from LocalAI's module graph -- cmd/protoc-gen-go-grpc is a
	# separate module, so `go mod vendor` never brings it -- which is why it
	# ships as its own self-contained vendored bundle.
	pushd protoc-gen-go-grpc > /dev/null || die
	GOFLAGS="-mod=vendor" ego build -o "${T}"/bin/protoc-gen-go-grpc \
		google.golang.org/grpc/cmd/protoc-gen-go-grpc
	popd > /dev/null || die

	# pkg/grpc/proto is generated, never shipped; backend/go/* import it.
	mkdir -p pkg/grpc/proto || die
	PATH="${T}/bin:${PATH}" protoc --experimental_allow_proto3_optional \
		-Ibackend/ \
		--go_out=pkg/grpc/proto/ --go_opt=paths=source_relative \
		--go-grpc_out=pkg/grpc/proto/ --go-grpc_opt=paths=source_relative \
		backend/backend.proto || die "protoc code generation failed"

	# core/http/app.go carries //go:embed react-ui/dist/*, so the UI is
	# mandatory: without dist/ the Go build fails outright rather than
	# producing a UI-less binary.
	pushd core/http/react-ui > /dev/null || die
	npm run build || die "react-ui vite build failed"
	popd > /dev/null || die

	# CGO_ENABLED=0 with an empty BUILD_TYPE is the CPU-only configuration:
	# no accelerator is linked in, matching the runtime-OCI backend model.
	# -trimpath keeps ${WORKDIR} out of the binary. Version and Commit are
	# normally filled from `git describe`/`git rev-parse`, neither of which
	# can work from a release tarball.
	local -x CGO_ENABLED=0
	ego build -trimpath \
		-ldflags "-X github.com/mudler/LocalAI/internal.Version=v${PV} -X github.com/mudler/LocalAI/internal.Commit=v${PV}" \
		-o local-ai ./cmd/local-ai
}

src_install() {
	dobin local-ai
	einstalldocs

	# Upstream ships no service files of any kind, so the project rule about
	# mirroring a systemd unit with an OpenRC script does not apply -- there is
	# no unit to mirror. This installs the OpenRC pair anyway, because local-ai
	# IS a daemon and without it the only way to run one is by hand, as root,
	# in whatever directory happens to be current.
	newinitd "${FILESDIR}"/local-ai.initd local-ai
	newconfd "${FILESDIR}"/local-ai.confd local-ai

	# 0640 root:root, not the 0644 newconfd leaves behind: this file is where
	# LOCALAI_API_KEY, the OIDC client secret and HF_TOKEN belong. root:root
	# rather than root:localai is deliberate and sufficient -- OpenRC sources
	# conf.d as root and only then drops to the localai user, so the daemon
	# never reads the file, and naming the group here would need the account to
	# exist on the BUILD host, which it need not.
	fperms 0640 /etc/conf.d/local-ai

	# Created by the init script at first start rather than here: fowners by
	# name would need the localai account on the build host.
	keepdir /var/lib/localai
}

pkg_postinst() {
	elog "Start the server with:"
	elog "    rc-service local-ai start"
	elog
	elog "It binds 127.0.0.1:8080 by default. That is not just hardening:"
	elog "local-ai's own default is the wildcard \":8080\", and it REFUSES to"
	elog "start on a wildcard or public address unless"
	elog "--allow-insecure-public-bind is passed. Configure authentication"
	elog "(LOCALAI_API_KEY or OIDC) in /etc/conf.d/local-ai before widening"
	elog "LOCALAI_ADDRESS."
	elog
	elog "Models, backends and generated content live under /var/lib/localai."
	elog "Inference backends are NOT built here: local-ai downloads them as OCI"
	elog "artifacts at runtime, picking one that matches the hardware it finds,"
	elog "so the server needs outbound network the first time a model is used"
	elog "and a CPU-only machine needs nothing extra."
}