mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-19 16:39:15 +00:00
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_<ver>_amd64.deb (systemd unit + adapters + example config)
llmsproxy-<ver>.x86_64.rpm
llmsproxy-<ver>-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.
This commit is contained in:
90
packaging/core-dist.sh
Executable file
90
packaging/core-dist.sh
Executable file
@ -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_<ver>_amd64.deb
|
||||
# cmd/build/dist/llmsproxy-<ver>.x86_64.rpm
|
||||
# cmd/build/dist/llmsproxy-<ver>-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"
|
||||
24
packaging/llmsproxy.service
Normal file
24
packaging/llmsproxy.service
Normal file
@ -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
|
||||
59
packaging/nfpm.yaml
Normal file
59
packaging/nfpm.yaml
Normal file
@ -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 <dev@modelrouter.local>
|
||||
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
|
||||
22
packaging/postinst.sh
Executable file
22
packaging/postinst.sh
Executable file
@ -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
|
||||
14
packaging/prerm.sh
Executable file
14
packaging/prerm.sh
Executable file
@ -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
|
||||
48
packaging/verify-dist.sh
Executable file
48
packaging/verify-dist.sh
Executable file
@ -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"
|
||||
Reference in New Issue
Block a user