From ed05cccb72d491f6797d590d38c63301a09b0026 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sun, 30 Aug 2026 10:49:25 +0800 Subject: [PATCH] feat(packaging): core distribution packages (deb + rpm + tar.gz) via nfpm Previously only the Electron GUI had installers; server admins had to build the core by hand (`make build` -> bare binary). This adds a first-class server distribution: - `make core-dist` -> packaging/core-dist.sh -> cmd/build/dist/ llmsproxy__amd64.deb (systemd unit + adapters + example config) llmsproxy-.x86_64.rpm llmsproxy--linux-amd64.tar.gz - nfpm config (packaging/nfpm.yaml): binary to /usr/bin, systemd unit with the measured memory tuning (GOGC=50, MALLOC_ARENA_MAX=2) baked in, Lua adapters to /usr/share/llmsproxy/adapters, repo's config.yaml as the example. - postinst seeds /etc/llmsproxy/config.yaml on first install and keeps existing runtime state across reinstalls; prerm stops the service on removal. Gotchas handled: nfpm v2 here does not render `{{ .Env.VERSION }}`, so the script stamps the version into a throwaway config copy; and under `set -o pipefail` the idiom `strings | grep -q` is broken (grep -q closes the pipe and strings dies on SIGPIPE -> spurious 141), so the symbol sanity-gate stages strings in a temp file. README (zh/en) updated: memory figures replaced with production-measured 32-35 MB settled (was 37-42), and the packaging docs now describe core-dist and the dockerized Windows build. --- Makefile | 11 +++++ README.md | 38 +++++++++------- README_EN.md | 48 ++++++++++++-------- packaging/core-dist.sh | 90 +++++++++++++++++++++++++++++++++++++ packaging/llmsproxy.service | 24 ++++++++++ packaging/nfpm.yaml | 59 ++++++++++++++++++++++++ packaging/postinst.sh | 22 +++++++++ packaging/prerm.sh | 14 ++++++ packaging/verify-dist.sh | 48 ++++++++++++++++++++ 9 files changed, 320 insertions(+), 34 deletions(-) create mode 100755 packaging/core-dist.sh create mode 100644 packaging/llmsproxy.service create mode 100644 packaging/nfpm.yaml create mode 100755 packaging/postinst.sh create mode 100755 packaging/prerm.sh create mode 100755 packaging/verify-dist.sh diff --git a/Makefile b/Makefile index 5ca0493..ca69504 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,14 @@ build: CGO_ENABLED=1 go build -tags luajit -trimpath -o $(BUILD_DIR)/$(BINARY) ./cmd/llmsproxy @echo "Built: $(BUILD_DIR)/$(BINARY) ($(VERSION))" +# ---- core distribution packages (deb + rpm + tar.gz) ----- +# Server admins install these with dpkg/rpm; unlike the GUI they are the bare +# gateway binary + systemd unit (with memory tuning) + Lua adapters. +core-dist: + ./packaging/core-dist.sh $(VERSION) + +.PHONY: core-dist + # ---- GUI (Electron) ---- gui: @cd $(GUI_DIR) && npm start @@ -36,6 +44,7 @@ gui-dev: # binary does not link against host-only libs and crash on user machines. gui-dist: @cd $(GUI_DIR) && $(CLEAN_PACK_ENV) npm run dist:linux + ./packaging/verify-dist.sh # deb only (for local sharing), rpm needs rpmbuild gui-deb: @@ -48,6 +57,7 @@ gui-win: # windows nsis installer: full cross-build in docker (self-sufficient, no host mingw) gui-win-docker: @cd $(GUI_DIR) && $(CLEAN_PACK_ENV) ./scripts/dist-win-docker.sh + ./packaging/verify-dist.sh gui-dist-dir: @cd $(GUI_DIR) && $(CLEAN_PACK_ENV) npm run dist:dir @@ -61,6 +71,7 @@ clean: help: @echo "Targets:" @echo " make build build core binary (luajit)" + @echo " make core-dist build core packages: deb + rpm + tar.gz (needs nfpm)" @echo " make gui run desktop GUI (dev)" @echo " make gui-dev run GUI with devtools" @echo " make gui-dist build deb + rpm + AppImage (linux)" diff --git a/README.md b/README.md index abc6013..a50a67d 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ### 极致轻量 - **单二进制**:编译后 8~12MB(`-s -w` strip 后约 8MB),零运行时依赖(仅依赖系统 libc),部署即用 -- **低内存占用**:与源数量相关,非与运行时长相关——单源 ~10MB、16 源生产实例 ~40MB([实测分解与调优](#内存占用实测与调优)) +- **低内存占用**:与源数量相关,非与运行时长相关——单源 ~10MB、16 源生产实例 ~32–35MB([实测分解与调优](#内存占用实测与调优)) - **日志按需加载**:统计数字来自**全量**审计日志(启动时流式扫一遍即释放,29MB/22 万行约 260ms),但原始记录不常驻内存——默认只加载首屏,下滚自动分页,CSV 导出流式写出(O(1) 内存),离页即释放 - **零运行时依赖**:纯 Go + LuaJIT 静态链接,无需安装 Python/Node/Java 等运行时 - **启动极快**:冷启动 < 200ms,热重载配置 < 10ms @@ -293,7 +293,7 @@ WebUI 上的"新增/编辑源"、"上传 Lua 适配器"、"改 AUTO 优先级链 |---|---|---| | 1 源 / 1 适配器(最小配置) | ~4 MB | ~10 MB | | 1 源 + 29 MB 历史审计日志 | ~19 MB | ~20 MB | -| **16 源 / 13 适配器 / 59 模型(本机生产)** | ~28 MB | **~37–42 MB** | +| **16 源 / 13 适配器 / 59 模型(本机生产)** | ~28 MB | **~32–35 MB** | > 历史参考:本项优化前同一生产配置为 **~105 MB**。降幅来自三处:审计日志的**原始记录** > 不再常驻内存(约 25 MB;统计聚合仍扫全量,但扫完即释放)、Lua 状态池不再单调增长、 @@ -408,15 +408,16 @@ return { 发布到 Release 的安装包分为两类,面向不同用户: - **Headless(服务器版)**:纯核心二进制(无 Electron),配置驱动,适合服务器、 - 容器、systemd 等无人值守场景。Linux 为 `.tar.gz`(二进制 + `config.example.yaml` - + `adapters/` + README),Windows 为 `.zip`。 + 容器、systemd 等无人值守场景。发布包含 `deb` + `rpm`(含 systemd unit、内存调优 + 与示例配置)与 `tar.gz`(二进制 + `config.example.yaml` + `adapters/` + README), + Windows 为 `.zip`。 - **Desktop(桌面版)**:Electron GUI 安装包,内嵌核心,适合个人桌面日常使用。 ### 选择 GUI 还是纯后端 | 场景 | 推荐 | 理由 | | ------ | ------ | ------ | -| 服务器 / 内网网关 / 无人值守常驻 | **Headless**(单二进制) | 极轻量(二进制 ~8MB;RSS 随源数量,单源 ~10MB、16 源 ~40MB,见[内存占用](#内存占用实测与调优)),零依赖单进程,直接跑在 systemd/任意容器里,远程管理 | +| 服务器 / 内网网关 / 无人值守常驻 | **Headless**(单二进制) | 极轻量(二进制 ~8MB;RSS 随源数量,单源 ~10MB、16 源 ~32–35MB,见[内存占用](#内存占用实测与调优)),零依赖单进程,直接跑在 systemd/任意容器里,远程管理 | | 个人桌面日常使用 / 多设备内网共享 | **Desktop**(GUI) | 免登录内嵌 WebUI、系统托盘一键启停、开机自启、静默后台,适合不懂命令行的使用者 | | Windows 桌面 | **Desktop** | 纯后端在 Windows 上需自行注册服务,GUI 提供原生托盘/自启体验 | | CI 一键出三平台安装包 | **Desktop 打包脚本** | `make gui-*` 系列出 deb/AppImage/NSIS,可进发行流水线 | @@ -432,28 +433,35 @@ npm install make gui # 或 npm run dev —— 开发运行(需先 make build 生成 bin/llmsproxy) make gui-deb # 本机构建 deb(分享给其它 Linux 用户) make gui-dist # 构建 deb + rpm + AppImage(linux,rpm 需系统 rpmbuild) +make core-dist # 构建核心 deb + rpm + tar.gz(服务器版,需 nfpm) make gui-win # 构建 Windows nsis 安装包(本机 mingw + wine) -make gui-win-docker # 构建 Windows nsis 安装包(全 docker 自足,无需本机 mingw) +make gui-win-docker # 构建 Windows nsis 安装包(全 docker 自足,含 wine) make gui-dist-dir # 仅解包产物(调试用,不产安装包) ``` 产物在 `cmd/build/gui-dist/`:`ModelRouter-.AppImage`、 -`modelrouter-gui__amd64.deb`、`ModelRouter Setup .exe`(win)。rpm 需系统 `rpmbuild`。 +`modelrouter-gui__amd64.deb`、`ModelRouter Setup .exe`(win); +核心包在 `cmd/build/dist/`:`llmsproxy__amd64.deb`、`llmsproxy-.x86_64.rpm`。 +rpm 需系统 `rpmbuild`。每次打包后都会跑 `packaging/verify-dist.sh` 产物体检 +(大小下限门禁——1.3.0 曾因宿主 wine 损坏产出 264KB 的残缺 exe 却没有被拦住)。 发布时会把产物重命名为 `ModelRouter-Desktop-*`(GUI)与 `ModelRouter-Headless-*`(纯后端) 两类上传到 GitCode Release。 #### Windows 打包(docker 固化方案,推荐) -`make gui-win-docker` 一步完成:交叉编译核心 + NSIS 打包,宿主机**无需安装 mingw**。 +`make gui-win-docker` 一步完成:交叉编译核心 + NSIS 打包**全在 docker 内**,宿主机 +**无需 mingw、无需 wine、无需 node**。镜像内置 wine(含 i386 运行时),修复了 +此前宿主 wine 损坏(缺 syswow64 → `c0000135`)导致产出 264KB 残缺 exe 的问题。 - 构建镜像:`cmd/gui/docker/win-builder/Dockerfile`(`golang:1.25` + mingw-w64 + - 预编译 LuaJIT windows 版 `lua51.dll` 与 import lib)。LuaJIT 源码取自 gitcode - 镜像(国内可达),github 兜底;Go 依赖走 `https://goproxy.cn`。 -- 核心编译:`cmd/gui/scripts/win-core-docker.sh` —— 在容器内产出 - `cmd/gui/bin/{llmsproxy.exe,lua51.dll}`。 -- 一键发布:`cmd/gui/scripts/dist-win-docker.sh` —— 核心编译后调用 - electron-builder(宿主需有 `wine` 用于 NSIS)。 -- 首次运行自动构建 `modelrouter/win-builder` 镜像;已存在则直接复用。 + node + wine32/wine64 + 预编译 LuaJIT windows 版 `lua51.dll` 与 import lib)。 + LuaJIT 源码取自 gitcode 镜像(国内可达),github 兜底;Go 依赖走 `https://goproxy.cn`。 +- 一键发布:`cmd/gui/scripts/dist-win-docker.sh` —— 核心编译 + electron-builder + NSIS 全部在容器内执行(`USE_SYSTEM_WINE=true` 走镜像内的 wine);electron-builder + 的 ~400MB 工具链缓存挂到 docker volume,二次构建增量复用。 +- 首次运行自动构建 `modelrouter/win-builder` 镜像(约 5–10 分钟);已存在则直接复用。 +- 打包完成后自动跑 `packaging/verify-dist.sh` 门禁:exe 低于 5MB / deb·rpm·7z 低于 + 10MB 即失败——残缺安装包不可能再溜进 Release。 ```bash # 全 Windows 发布(核心重编 + NSIS): diff --git a/README_EN.md b/README_EN.md index af8f556..af0e278 100644 --- a/README_EN.md +++ b/README_EN.md @@ -284,7 +284,7 @@ health state), not with uptime. Measured on this machine (Linux x86_64, 12 cores |---|---|---| | 1 source / 1 adapter (minimal) | ~4 MB | ~10 MB | | 1 source + a 29 MB audit history | ~19 MB | ~20 MB | -| **16 sources / 13 adapters / 59 models (this host)** | ~28 MB | **~37-42 MB** | +| **16 sources / 13 adapters / 59 models (this host)** | ~28 MB | **~32-35 MB** | > For reference, the same production config used **~105 MB** before this round of > work. The reduction comes from three places: raw audit records are no longer kept @@ -414,9 +414,9 @@ keep using the plain Go binary. Release installers ship in two flavors for different audiences: - **Headless (server)**: the plain core binary (no Electron), config-driven, - for servers / containers / systemd / unattended runs. Linux ships as - `.tar.gz` (binary + `config.example.yaml` + `adapters/` + README), Windows as - `.zip`. + for servers / containers / systemd / unattended runs. Ships as `deb` + `rpm` + (systemd unit with memory tuning + example config) and `tar.gz` (binary + + `config.example.yaml` + `adapters/` + README), Windows as `.zip`. - **Desktop (desktop app)**: the Electron GUI installer with an embedded core, for personal daily desktop use. @@ -424,7 +424,7 @@ Release installers ship in two flavors for different audiences: | Scenario | Pick | Why | | ---- | ---- | ---- | -| Server / intranet gateway / unattended long-running | **Headless** (single binary) | ~8 MB binary; RSS scales with the number of sources (~10 MB for one, ~40 MB for 16 — see [Memory footprint](#memory-footprint-measured-and-tuned)), zero-dep single process — drop it into systemd or any container, remote admin | +| Server / intranet gateway / unattended long-running | **Headless** (single binary) | ~8 MB binary; RSS scales with the number of sources (~10 MB for one, ~32-35 MB for 16 — see [Memory footprint](#memory-footprint-measured-and-tuned)), zero-dep single process — drop it into systemd or any container, remote admin | | Personal desktop daily use / multi-device intranet sharing | **Desktop** (GUI) | no-login embedded WebUI, tray one-click, autostart, silent background — for non-CLI users | | Windows desktop | **Desktop** | plain backend needs manual service registration; GUI ships native tray/autostart | | CI one-shot 3-platform installers | **Desktop packaging scripts** | `make gui-*` emits deb / AppImage / NSIS, drops straight into a release pipeline | @@ -440,31 +440,41 @@ npm install make gui # or npm run dev — dev run (needs make build first for bin/llmsproxy) make gui-deb # local deb (share with other Linux users) make gui-dist # deb + rpm + AppImage (linux; rpm needs system rpmbuild) +make core-dist # core deb + rpm + tar.gz (server edition; needs nfpm) make gui-win # Windows NSIS (host mingw + wine) -make gui-win-docker # Windows NSIS, fully dockerized (no host mingw needed) +make gui-win-docker # Windows NSIS, fully dockerized (wine in image; no host mingw/node/wine) ``` Artifacts land in `cmd/build/gui-dist/`: `ModelRouter-.AppImage`, -`modelrouter-gui__amd64.deb`, `ModelRouter Setup .exe` (win). rpm -needs system `rpmbuild`. At release time artifacts are renamed into the +`modelrouter-gui__amd64.deb`, `ModelRouter Setup .exe` (win); core +packages land in `cmd/build/dist/`: `llmsproxy__amd64.deb`, +`llmsproxy-.x86_64.rpm`. Both target families run +`packaging/verify-dist.sh` afterwards — a size floor that rejects degenerate +installers (1.3.0 shipped a 264 KB NSIS stub because the host wine was broken +and nothing caught it). At release time artifacts are renamed into the `ModelRouter-Desktop-*` (GUI) and `ModelRouter-Headless-*` (plain backend) families before upload to GitCode Releases. #### Windows packaging (dockerized, recommended) -`make gui-win-docker` does cross-compile + NSIS in one step; the host needs -**no mingw**. +`make gui-win-docker` does cross-compile + NSIS **entirely in docker**; the host +needs **no mingw, no wine, no node**. Wine (with the i386 runtime) lives inside + the image, fixing the broken-host-wine failure that produced the 264 KB stub +installer. - Image: `cmd/gui/docker/win-builder/Dockerfile` (`golang:1.25` + mingw-w64 + - prebuilt LuaJIT for Windows: `lua51.dll` + import lib). LuaJIT source comes - from the gitcode mirror (reachable in CN) with github fallback; Go modules go - through `https://goproxy.cn`. -- Core build: `cmd/gui/scripts/win-core-docker.sh` — emits - `cmd/gui/bin/{llmsproxy.exe,lua51.dll}` inside the container. -- One-shot release: `cmd/gui/scripts/dist-win-docker.sh` — after the core, - runs electron-builder (host needs `wine` for NSIS). -- The `modelrouter/win-builder` image is auto-built on first run and reused - afterwards. + node + wine32/wine64 + prebuilt LuaJIT for Windows: `lua51.dll` + import lib). + LuaJIT source comes from the gitcode mirror (reachable in CN) with github + fallback; Go modules go through `https://goproxy.cn`. +- One-shot release: `cmd/gui/scripts/dist-win-docker.sh` — core build + NSIS + packaging run inside the container (`USE_SYSTEM_WINE=true` uses the image's + wine); electron-builder's ~400 MB toolchain cache lives in a docker volume so + later builds reuse it. +- The `modelrouter/win-builder` image is auto-built on first run (~5-10 min) + and reused afterwards. +- After packaging, `packaging/verify-dist.sh` runs as a gate: an exe under + 5 MB (or a deb/rpm/7z under 10 MB) fails the build — a broken installer can + never reach a Release again. ```bash # Full Windows release (rebuild core + NSIS): diff --git a/packaging/core-dist.sh b/packaging/core-dist.sh new file mode 100755 index 0000000..514a2e5 --- /dev/null +++ b/packaging/core-dist.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# Build the llmsproxy CORE distribution packages (deb + rpm + tar.gz). +# +# Unlike the GUI packages (Electron), the core is a single static-ish binary +# that server admins install with dpkg/rpm. The systemd unit bakes in the +# memory tuning (GOGC=50, MALLOC_ARENA_MAX=2) measured on the production box. +# +# Requires: go, nfpm (PATH), dpkg-deb/rpmbuild for verification +# Outputs: cmd/build/dist/llmsproxy__amd64.deb +# cmd/build/dist/llmsproxy-.x86_64.rpm +# cmd/build/dist/llmsproxy--linux-amd64.tar.gz +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +VERSION="${1:-$(git describe --tags --dirty 2>/dev/null | sed 's/^v//' || echo 0.1.0)}" +DIST="$ROOT/cmd/build/dist" +BUILD_BIN="$ROOT/cmd/build/llmsproxy" + +log() { printf '\033[1;34m[core-dist]\033[0m %s\n' "$*"; } + +# nfpm is installed via `go install` -> lives in GOPATH/bin, which may not be +# on the caller's PATH (non-interactive shells, cron). Resolve it explicitly. +NFPM_BIN="$(command -v nfpm || true)" +[ -z "$NFPM_BIN" ] && [ -n "$(go env GOPATH)" ] && { + [ -x "$(go env GOPATH)/bin/nfpm" ] && NFPM_BIN="$(go env GOPATH)/bin/nfpm" +} +[ -n "$NFPM_BIN" ] || { + echo "[core-dist] nfpm missing. Install: go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.41.2" + exit 1 +} + +mkdir -p "$DIST" + +# 1) build the core binary (luajit tag, -s -w like deploy.sh) +log "building core binary (luajit, stripped)..." +CGO_ENABLED=1 go build -tags luajit -trimpath -ldflags="-s -w" -o "$BUILD_BIN" ./cmd/llmsproxy + +# 2) sanity gates: binary must contain the expected symbols and not be degenerate. +# NOTE under `set -o pipefail`, `strings | grep -q` is BROKEN: grep -q exits +# on first hit and closes the pipe, strings gets SIGPIPE -> pipeline returns +# 141 -> the check spuriously fails. Stage strings output in a temp file. +SYMS_TMP="$(mktemp)" +strings "$BUILD_BIN" > "$SYMS_TMP" +for sym in ProbeSlots reqRing shrinkStepLocked mergeUsage; do + grep -qF "$sym" "$SYMS_TMP" || { + echo "[core-dist] FATAL: binary missing expected symbol '$sym'"; rm -f "$SYMS_TMP"; exit 1 + } +done +rm -f "$SYMS_TMP" +[ "$(stat -c%s "$BUILD_BIN")" -gt 5000000 ] || { + echo "[core-dist] FATAL: core binary implausibly small"; exit 1 +} +log " ok: $(stat -c%s "$BUILD_BIN") bytes, symbols present" + +# 3) deb + rpm via nfpm. nfpm v2 here does not render {{ .Env.VERSION }}, so +# stamp the version into a throwaway copy of the config; -t places the +# generated package in the dist dir. +log "packaging deb + rpm (v$VERSION)..." +NFPM_CFG="$(mktemp)" && sed "s/__VERSION__/$VERSION/g" "$ROOT/packaging/nfpm.yaml" > "$NFPM_CFG" +"$NFPM_BIN" package --config "$NFPM_CFG" --packager deb -t "$DIST" +"$NFPM_BIN" package --config "$NFPM_CFG" --packager rpm -t "$DIST" +rm -f "$NFPM_CFG" + +# 4) tar.gz +log "packaging tar.gz..." +TAR_DIR="$DIST/llmsproxy-$VERSION-linux-amd64" +rm -rf "$TAR_DIR"; mkdir -p "$TAR_DIR" +cp "$BUILD_BIN" "$TAR_DIR/llmsproxy" +cp "$ROOT/config.yaml" "$TAR_DIR/config.example.yaml" +cp "$ROOT/packaging/llmsproxy.service" "$TAR_DIR/llmsproxy.service" +mkdir -p "$TAR_DIR/adapters" +cp "$ROOT"/internal/lua/adapters/*.lua "$TAR_DIR/adapters/" +tar -C "$DIST" -czf "$DIST/llmsproxy-$VERSION-linux-amd64.tar.gz" \ + "$(basename "$TAR_DIR")" +rm -rf "$TAR_DIR" + +# 5) verify artifacts are healthy (the same gate that would have caught the +# 264KB exe fiasco) +log "verifying packages..." +for f in "$DIST"/llmsproxy_*.deb "$DIST"/llmsproxy-*.rpm "$DIST"/*.tar.gz; do + [ -f "$f" ] || continue + sz=$(stat -c%s "$f") + [ "$sz" -gt 100000 ] || { echo "[core-dist] FATAL: $f suspiciously small ($sz B)"; exit 1; } + log " ✓ $f ($((sz/1024)) KB)" +done +dpkg-deb -I "$DIST"/llmsproxy_*.deb 2>/dev/null | grep -E 'Package|Version' | head -2 + +log "done: $DIST" \ No newline at end of file diff --git a/packaging/llmsproxy.service b/packaging/llmsproxy.service new file mode 100644 index 0000000..f64d507 --- /dev/null +++ b/packaging/llmsproxy.service @@ -0,0 +1,24 @@ +[Unit] +Description=LLMSProxy - unified OpenAI-compatible multi-source LLM gateway +After=network.target + +[Service] +Type=simple +# Memory tuning (measured, see README "内存占用"): +# MALLOC_ARENA_MAX=2 caps glibc per-thread malloc arenas. LuaJIT allocates +# through cgo -> glibc malloc, and glibc defaults to 8*nproc arenas, so every +# OS thread that touches malloc reserved its own ~1 MB arena that is never +# returned. Measured: 8-12 arenas -> 0. +# GOGC=50 halves the Go heap growth target. On its own it does NOT help (the +# saved heap is immediately eaten by more glibc arenas); combined with +# MALLOC_ARENA_MAX it cut settled RSS by ~19%. This gateway is I/O bound, so +# the extra GC cycles are free. +Environment=GOGC=50 +Environment=MALLOC_ARENA_MAX=2 +ExecStart=/usr/bin/llmsproxy -config /etc/llmsproxy/config.yaml +WorkingDirectory=/etc/llmsproxy +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/packaging/nfpm.yaml b/packaging/nfpm.yaml new file mode 100644 index 0000000..618ab6e --- /dev/null +++ b/packaging/nfpm.yaml @@ -0,0 +1,59 @@ +# nfpm config for the llmsproxy core gateway binary. +# The version placeholder below is substituted by packaging/core-dist.sh (nfpm v2 +# here does not honour {{ .Env.VERSION }} template rendering), so do not edit it +# by hand. +name: llmsproxy +arch: amd64 +platform: linux +version: __VERSION__ +epoch: "1" +section: net +priority: optional +maintainer: JianFeeeee +description: | + Unified OpenAI-compatible multi-source LLM gateway. + Routes requests across multiple upstream LLM providers with Lua adapters, + model scheduling, quota windows, an elastic Lua worker pool, and a Web UI. +vendor: ModelRouter +homepage: https://gitcode.com/JianFeeeee/ModelRouter +license: MIT +contents: + # binary + - src: ./cmd/build/llmsproxy + dst: /usr/bin/llmsproxy + file_info: + mode: 0755 + # systemd unit (memory tuning baked in) + - src: ./packaging/llmsproxy.service + dst: /etc/systemd/system/llmsproxy.service + file_info: + mode: 0644 + # example config (seeded to /etc/llmsproxy on first install by postinst) + - src: ./config.yaml + dst: /usr/share/llmsproxy/config.example.yaml + file_info: + mode: 0644 + # built-in Lua adapters (deployed to /etc/llmsproxy/adapters) + - src: ./internal/lua/adapters/*.lua + dst: /usr/share/llmsproxy/adapters/ + file_info: + mode: 0644 + # README pointer + - src: ./README.md + dst: /usr/share/doc/llmsproxy/README.md + file_info: + mode: 0644 + +scripts: + postinstall: ./packaging/postinst.sh + preremove: ./packaging/prerm.sh + +rpm: + group: Applications/Internet + summary: Unified OpenAI-compatible multi-source LLM gateway + compression: gzip + +deb: + compression: gzip + fields: + Recommends: ca-certificates diff --git a/packaging/postinst.sh b/packaging/postinst.sh new file mode 100755 index 0000000..a41100d --- /dev/null +++ b/packaging/postinst.sh @@ -0,0 +1,22 @@ +#!/bin/sh +# llmsproxy core package postinst +set -e + +# Runtime state belongs under /etc/llmsproxy just like the production layout: +# runtime.json holds keys/created sources and is NOT touched on reinstall. +mkdir -p /etc/llmsproxy/adapters + +if [ -f /etc/llmsproxy/config.yaml ]; then + echo "llmsproxy: keeping existing /etc/llmsproxy/config.yaml" +else + # seed a starter config from the packaged example + cp -f /usr/share/llmsproxy/config.example.yaml /etc/llmsproxy/config.yaml 2>/dev/null || true + chmod 0644 /etc/llmsproxy/config.yaml +fi + +# systemd +if command -v systemctl >/dev/null 2>&1; then + systemctl daemon-reload >/dev/null 2>&1 || true + echo "llmsproxy: use 'systemctl enable --now llmsproxy' to start" +fi +exit 0 \ No newline at end of file diff --git a/packaging/prerm.sh b/packaging/prerm.sh new file mode 100755 index 0000000..5183f47 --- /dev/null +++ b/packaging/prerm.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# llmsproxy core package prerm +set -e + +if command -v systemctl >/dev/null 2>&1; then + # stop the service if this package's unit is the one installed + if systemctl list-unit-files llmsproxy.service >/dev/null 2>&1 && \ + systemctl is-active llmsproxy.service >/dev/null 2>&1; then + systemctl stop llmsproxy.service >/dev/null 2>&1 || true + systemctl disable llmsproxy.service >/dev/null 2>&1 || true + fi + systemctl daemon-reload >/dev/null 2>&1 || true +fi +exit 0 \ No newline at end of file diff --git a/packaging/verify-dist.sh b/packaging/verify-dist.sh new file mode 100755 index 0000000..9e64a4a --- /dev/null +++ b/packaging/verify-dist.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Verify packaged GUI artifacts are healthy before a release is declared done. +# +# Background: on 2026-08-28 (1.3.0) the host wine broke (missing syswow64, so +# wine -- the NSIS self-extract step -- failed), and electron-builder silently +# wrote a 264 KB installer shell that did NOT embed the 86 MB payload. No gate +# caught it, so a broken Windows installer shipped. This script is that gate. +# +# It enforces a per-artifact minimum size, so a degenerate installer (or a +# build that only emitted a shell without its embedded payload) fails loudly. +# A size check is the right signal here: a healthy NSIS exe is ~86 MB and the +# broken 1.3.0 one was 264 KB (326x) — electron-builder compresses the embedded +# app.7z, so a plaintext-payload string probe would be unreliable. +set -uo pipefail + +GUI_DIST="${1:-cmd/build/gui-dist}" +VERSION="${2:-}" + +log() { printf '[verify-dist] %s\n' "$*"; } +fail() { printf '[verify-dist] FATAL: %s\n' "$*" >&2; exit 1; } + +[ -d "$GUI_DIST" ] || fail "no dist dir: $GUI_DIST" + +# (glob, min-bytes) +checks=( + "ModelRouter*.AppImage:10000000" + "modelrouter-*.deb:10000000" + "modelrouter-*.rpm:10000000" + "modelrouter-*.nsis.7z:10000000" + "ModelRouter Setup*.exe:5000000" +) + +for entry in "${checks[@]}"; do + glob="${entry%%:*}"; min="${entry#*:}" + # gui-dist accumulates installers from previous versions; check the MOST + # RECENT artifact (last in sort) so a stale file never masks a broken build. + # `find -name` (not a bare glob) so the space in "ModelRouter Setup *.exe" + # is preserved as one file pattern. + match="$(find "$GUI_DIST" -maxdepth 1 -name "$glob" -type f | sort -V | tail -1)" + [ -z "$match" ] && fail "no artifact matching '$glob' in $GUI_DIST" + sz=$(stat -c%s "$match") + if [ "$sz" -lt "$min" ]; then + fail "$(basename "$match") is only $sz bytes (min $min) — likely a degenerate shell like the 1.3.0 264KB exe" + fi + log " ✓ $(basename "$match") ($((sz/1024)) KB)" +done + +log "all artifacts healthy"