# Copyright 1999-2026 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 EAPI=8 # HYBRID PACKAGING -- DO NOT "SIMPLIFY" THIS ON A VERSION BUMP. # # 1) Why the binaries come from the "embeddable" tarball and NOT from the .deb: # # 1a) THE REASON THAT OUTLIVES THE SONAMES -- the .deb's lemond links # libsystemd.so.0. Measured 2026-08-08 on the 11.5.2 payload: # readelf -dW /usr/bin/lemond | grep NEEDED # libsystemd.so.0 libcap.so.2 libcpp-httplib.so.0.41 ... # imports: sd_bus_open_system sd_bus_call_method sd_bus_message_read # sd_bus_message_unref sd_bus_error_free sd_bus_unref # sd_pid_get_unit # DT_NEEDED is a hard load-time requirement, so that binary does not even # start on a host without systemd's libraries -- it merges cleanly, passes # QA, and then fails at exec on an OpenRC box. That is the exact failure # sci-biology/foldingathome hit between 8.5.5 and 8.5.6. The embeddable # lemond links neither libsystemd nor libcap (same command, same day). # This is listed FIRST because the soname reasons below are fixable -- # someone could package cpp-httplib 0.41 -- and this one is not. If a # future bump makes the .deb payload look tempting again, re-run the readelf # above before touching src_install. # # 1b) Debian-13 sonames. Upstream's .deb (and the Fedora RPMs) link against # sonames that do not exist on Gentoo: # libcpp-httplib.so.0.41 -- Gentoo builds cpp-httplib as .so.0.50.1 # libwebsockets.so.19 -- Gentoo ships libwebsockets.so.21 # libmbedcrypto.so.16 -- Gentoo installs this as libmbedcrypto-3.so.16 # The "embeddable" tarball links those statically and only needs libraries # Gentoo provides under the very same soname (verified with readelf -d): # libz.so.1 libzstd.so.1 libssl.so.3 libcrypto.so.3 libdrm_amdgpu.so.1 # libstdc++.so.6 libm.so.6 libgcc_s.so.1 libc.so.6 # # 2) Why the .deb is still fetched (unconditionally, amd64 flavour only): # Only *arch-independent data* is taken out of it -- never an ELF object. # The embeddable tarball omits the web UI, the JSON schemas, the examples, # the man pages, the systemd units and architecture_defaults.json. Those # files carry no machine code, so the amd64 .deb serves every arch and can # stay outside the SRC_URI USE-conditionals. # The six resources/*.json present in BOTH archives were verified # byte-identical for 11.5.0 (backend_versions, bench_scenarios, defaults, # server_models, toolDefinitions, vllm_model_config), so the .deb copy is # used for the whole resources/ tree -- it is a strict superset of the # tarball's. Re-check that with cmp before trusting it on the next bump. # # 3) Why the ELF payload lives in /usr/$(get_libdir)/lemonade-server and NOT # directly in /usr/bin: # The embeddable build resolves its data files as / # resources/ ONLY. Unlike the .deb build it has no /usr/share/lemonade-server # fallback compiled in -- grep both binaries: that string is present in the # .deb one and absent from the embeddable one. Installed straight into # /usr/bin it dies at startup with # Error: Failed to open /usr/bin/resources/defaults.json # So the real executables sit next to a "resources" symlink pointing at the # arch-independent tree under /usr/share/lemonade-server, and /usr/bin only # carries symlinks (/proc/self/exe resolves through them to the real path, # so the lookup still lands in the right directory). inherit multilib systemd unpacker DESCRIPTION="Local LLM server with GPU and NPU acceleration (prebuilt binaries)" HOMEPAGE="https://lemonade-server.ai/ https://github.com/lemonade-sdk/lemonade" MY_EMB="lemonade-embeddable-${PV}-ubuntu" LEMONADE_URI="https://github.com/lemonade-sdk/lemonade/releases/download/v${PV}" SRC_URI=" amd64? ( ${LEMONADE_URI}/${MY_EMB}-x64.tar.gz -> ${P}-amd64.tar.gz ) arm64? ( ${LEMONADE_URI}/${MY_EMB}-arm64.tar.gz -> ${P}-arm64.tar.gz ) ${LEMONADE_URI}/lemonade-server_${PV}-debian13_amd64.deb -> ${P}-data.deb " S="${WORKDIR}" # resources/web-app/renderer.bundle.js is the webpack bundle upstream ships # prebuilt: React and KaTeX (MIT), highlight.js (BSD-3-Clause) and some # Apache-2.0 pieces, per the .LICENSE.txt installed next to it. LICENSE="Apache-2.0 BSD MIT" SLOT="0" KEYWORDS="-* ~amd64 ~arm64" IUSE="+systemd" RESTRICT="bindist mirror strip" RDEPEND=" !!sci-ml/lemonade acct-group/lemonade acct-user/lemonade app-arch/unzip app-arch/zstd:= dev-libs/openssl:0/3 sys-libs/zlib:= x11-libs/libdrm[video_cards_amdgpu] " # ar(1) comes from binutils (@system); xz-utils decompresses the deb data.tar.xz. BDEPEND="app-arch/xz-utils" QA_PREBUILT="*" # src_unpack comes from unpacker.eclass: it routes the .tar.gz through the # normal unpacker and the .deb through unpack_deb (ar + data.tar.xz), so no # override is needed here. src_install() { local libdir="/usr/$(get_libdir)/lemonade-server" local datadir="/usr/share/lemonade-server" local emb if use amd64; then emb="${WORKDIR}/${MY_EMB}-x64" elif use arm64; then emb="${WORKDIR}/${MY_EMB}-arm64" else die "unsupported ARCH=${ARCH}: no embeddable tarball for it" fi # --- ELF payload (arch dependent, from the embeddable tarball) --------- exeinto "${libdir}" doexe "${emb}/lemonade" doexe "${emb}/lemond" # --- arch-independent resources (from the .deb) ------------------------ insinto "${datadir}" doins -r usr/share/lemonade-server/resources # The binaries look for "/resources"; point that at the shared # tree so the data is installed FHS-correctly but still found at runtime. dosym -r "${datadir}/resources" "${libdir}/resources" # User-facing commands. dosym -r "${libdir}/lemonade" /usr/bin/lemonade dosym -r "${libdir}/lemond" /usr/bin/lemond # lemond additionally probes this absolute path as a system-wide defaults # file (hardcoded string "/usr/share/lemonade/defaults.json"). It is # byte-identical to resources/defaults.json; ship it so it resolves. insinto /usr/share/lemonade doins usr/share/lemonade/defaults.json # Examples keep upstream's layout: they are referenced from the docs and # some are meant to be run, so no dodoc compression. insinto "${datadir}" doins -r usr/share/lemonade-server/examples fperms +x "${datadir}/examples/migrate-to-systemd.sh" # --- man pages --------------------------------------------------------- # The .deb ships them pre-gzipped; portage does its own compression and # warns about compressed files in docompress-ed directories, so unpack # them first. gunzip usr/share/man/man1/lemonade.1.gz usr/share/man/man1/lemond.1.gz || die doman usr/share/man/man1/lemonade.1 doman usr/share/man/man1/lemond.1 # --- service files ----------------------------------------------------- # One service per scope, mirroring the two units upstream ships. Both are # installed UNCONDITIONALLY: an init script costs a systemd user nothing, # while gating it would leave an OpenRC user with no way to run the daemon. # Only the units are gated by USE=systemd. # # The system pair is byte-identical to sci-ml/lemonade's -- same # /usr/bin/lemond, same acct-user/lemonade -- because FILESDIR is # per-package and the file cannot be shared. Keep them in sync. # # An earlier revision claimed no OpenRC service was possible here because # the units use StateDirectory and AmbientCapabilities. That was wrong on # both counts: StateDirectory/RuntimeDirectory map onto `checkpath -d` in # start_pre(), which lemond.initd already does, and OpenRC does have a # `capabilities` variable (supervise-daemon --capabilities). # # CAP_SYS_RESOURCE is deliberately NOT set, and that is now a measurement # rather than a preference. Run 2026-08-08: lemond started with zero # capabilities and RLIMIT_MEMLOCK soft = hard = 8 MiB, bound its HTTP and # WebSocket servers and built its 114-entry model cache, with no capability, # rlimit or memlock diagnostic. Upstream's own systemd USER unit ships with # no AmbientCapabilities at all over the same ExecStart, so upstream already # runs this daemon without it in one of its two scopes. The binary does # call setrlimit, so the capability is not decorative upstream -- it is what # would permit raising a HARD limit -- but the only memlock diagnostics in # it (memlock_ok, "Memlock limits are too low.") sit in an NPU-readiness # block next to npu_driver_ok and "NPU validation failed.", i.e. they report # a condition rather than gate startup. # NOT covered: the test host has no NPU, so that probe was never reached. # On a Ryzen AI box with a restrictive memlock ceiling the OpenRC answer is # an admin knob -- rc_ulimit="-l unlimited" in /etc/conf.d/lemond, or # /etc/security/limits.conf -- not a capability in this file. newinitd "${FILESDIR}"/lemond.initd lemond newconfd "${FILESDIR}"/lemond.confd lemond # User scope. newinitd has no user-scope variant, so the script is installed # as a plain executable, following sys-apps/xdg-desktop-portal. exeinto /etc/user/init.d newexe "${FILESDIR}"/lemond-user.initd lemond # --- systemd units ----------------------------------------------------- # acct-user/acct-group stay unconditional -- the daemon is meant to run # under a dedicated account however it is started. if use systemd; then systemd_dounit usr/lib/systemd/system/lemond.service systemd_douserunit usr/lib/systemd/user/lemond.service fi # upstream's sysusers.d drop-in is deliberately not installed: the account # comes from acct-user/lemonade and acct-group/lemonade, same as in # sci-ml/lemonade. # --- configuration ----------------------------------------------------- # Read by the unit via EnvironmentFile=-/etc/lemonade/conf.d/*.conf insinto /etc/lemonade/conf.d doins etc/lemonade/conf.d/zz-secrets.conf } pkg_postinst() { elog "Start the system-wide server with one of:" elog " rc-service lemond start" elog " systemctl enable --now lemond.service" elog elog "Or run it as your own user (models land in ~/.cache/lemonade):" elog " rc-service --user lemond start" elog " systemctl --user enable --now lemond.service" elog elog "The web UI is served at http://localhost:13305/ once lemond is up." elog "API keys and HF_TOKEN belong in /etc/lemonade/conf.d/zz-secrets.conf" elog "for the systemd unit, or in /etc/conf.d/lemond for the OpenRC service." elog elog "Inference backends (llama.cpp, ROCm, vLLM, ...) are downloaded by" elog "lemond at runtime into its cache directory; they are not packaged." }