Files
ModelRouter/packaging/core-dist.sh
JianFeeeee e2ced5ed66 fix(packaging): 包内 unit 指向 /usr/bin;core-dist 支持无 nfpm 打包
两个都是发布链上的真缺陷,构建 v3.0.0 产物时才暴露:

1. **unit 与包内二进制路径不一致**:deb/rpm 把二进制装到 `/usr/bin/modelrouter`,
   而 `modelrouter.service` 的 `ExecStart` 写 `/usr/local/bin/modelrouter`。
   装包后 `systemctl start` 只会给 203/EXEC,看起来像"装好了但起不来"。
   - 包内 unit 改 `/usr/bin/modelrouter`(FHS,装包即用)
   - 源码部署仍用 `/usr/local/bin`,`deploy.sh` 增加校验:unit 的 ExecStart
     指向不存在的文件时**中止**并说明两种布局的区别
   - docs/deployment.md 同步说明

2. **core-dist 硬依赖 nfpm**:构建机离线(GOPROXY 不可达)时装不上 nfpm,
   发布直接被堵死。现在 nfpm 优先,缺失时回退 dpkg-deb + rpmbuild,
   两者产出同一套文件布局,并继续由末尾的 archive-content 门禁校验。
   - rpm 回退踩到的坑:postinst/prerm 末尾无换行时,`%preun` 会粘在
     `exit 0` 后面,整个 `%files` 段被吞,rpmbuild 报
     "Installed (but unpackaged) file(s) found"。
   - `%dir /etc/modelrouter` 要求 buildroot 里存在该目录,否则
     "Directory not found"。

验证:`./packaging/core-dist.sh 3.0.0` 产出 deb(4220KB)/rpm(4229KB)/tar(4145KB);
包内 unit `ExecStart=/usr/bin/modelrouter`,`systemd-analyze verify --root` 通过;
deb/rpm 均不含 `plugins/`,adapters=14。
2026-10-27 00:51:12 +08:00

252 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Build the modelrouter 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/modelrouter_<ver>_amd64.deb
# cmd/build/dist/modelrouter-<ver>.x86_64.rpm
# cmd/build/dist/modelrouter-<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/modelrouter"
log() { printf '\033[1;34m[core-dist]\033[0m %s\n' "$*"; }
# nfpm is the primary packager, but it is a `go install` artifact and the build
# host may be offline (no GOPROXY) — a release must not depend on being able to
# fetch it. Fall back to dpkg-deb / rpmbuild, which are present wherever the
# packages are actually verified. Both paths produce the same file layout.
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"
}
if [ -n "$NFPM_BIN" ]; then
log "packager: nfpm ($NFPM_BIN)"
else
command -v dpkg-deb >/dev/null || { echo "[core-dist] neither nfpm nor dpkg-deb available"; exit 1; }
command -v rpmbuild >/dev/null || { echo "[core-dist] neither nfpm nor rpmbuild available"; exit 1; }
log "packager: nfpm missing -> dpkg-deb + rpmbuild fallback"
fi
mkdir -p "$DIST"
# ---- fallback packagers -------------------------------------------------
# Same file layout as packaging/nfpm.yaml. Kept in this file (not a separate
# script) so the two paths cannot drift silently: both are exercised by the
# archive-content gate at the end.
package_deb_fallback() {
local root="$ROOT/cmd/build/pkgroot-deb"
rm -rf "$root"
mkdir -p "$root/DEBIAN" "$root/usr/bin" "$root/etc/systemd/system" \
"$root/usr/share/modelrouter/adapters" "$root/usr/share/doc/modelrouter"
install -m 0755 "$BUILD_BIN" "$root/usr/bin/modelrouter"
install -m 0644 "$ROOT/packaging/modelrouter.service" "$root/etc/systemd/system/modelrouter.service"
install -m 0644 "$ROOT/packaging/config.example.yaml" "$root/usr/share/modelrouter/config.example.yaml"
install -m 0644 "$ROOT"/internal/lua/adapters/*.lua "$root/usr/share/modelrouter/adapters/"
install -m 0755 "$ROOT/packaging/install-browser-stack.sh" "$root/usr/share/modelrouter/install-browser-stack.sh"
install -m 0644 "$ROOT/README.md" "$root/usr/share/doc/modelrouter/README.md"
install -m 0755 "$ROOT/packaging/postinst.sh" "$root/DEBIAN/postinst"
install -m 0755 "$ROOT/packaging/prerm.sh" "$root/DEBIAN/prerm"
local inst_size
inst_size=$(du -sk "$root" | awk '{print $1}')
cat > "$root/DEBIAN/control" <<EOF
Package: modelrouter
Version: $VERSION
Section: net
Priority: optional
Architecture: amd64
Installed-Size: $inst_size
Maintainer: JianFeeeee <dev@modelrouter.local>
Recommends: ca-certificates
Homepage: https://gitcode.com/JianFeeeee/ModelRouter
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.
EOF
dpkg-deb --root-owner-group -Zgzip --build "$root" \
"$DIST/modelrouter_${VERSION}_amd64.deb" >/dev/null
log " built $(basename "$DIST/modelrouter_${VERSION}_amd64.deb")"
}
package_rpm_fallback() {
local top="$ROOT/cmd/build/rpmbuild"
rm -rf "$top"
mkdir -p "$top"/{BUILD,RPMS,SOURCES,SPECS,BUILDROOT}
local spec="$top/SPECS/modelrouter.spec"
{
cat <<EOF
Name: modelrouter
Version: $VERSION
Release: 1
Summary: Unified OpenAI-compatible multi-source LLM gateway
License: MIT
URL: https://gitcode.com/JianFeeeee/ModelRouter
BuildArch: x86_64
Requires: ca-certificates
AutoReqProv: no
%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.
%install
mkdir -p %{buildroot}/usr/bin %{buildroot}/etc/systemd/system \\
%{buildroot}/usr/share/modelrouter/adapters \\
%{buildroot}/usr/share/doc/modelrouter %{buildroot}/etc/modelrouter
install -m 0755 $BUILD_BIN %{buildroot}/usr/bin/modelrouter
install -m 0644 $ROOT/packaging/modelrouter.service %{buildroot}/etc/systemd/system/modelrouter.service
install -m 0644 $ROOT/packaging/config.example.yaml %{buildroot}/usr/share/modelrouter/config.example.yaml
install -m 0644 $ROOT/internal/lua/adapters/*.lua %{buildroot}/usr/share/modelrouter/adapters/
install -m 0755 $ROOT/packaging/install-browser-stack.sh %{buildroot}/usr/share/modelrouter/install-browser-stack.sh
install -m 0644 $ROOT/README.md %{buildroot}/usr/share/doc/modelrouter/README.md
%post
EOF
sed '1{/^#!/d}' "$ROOT/packaging/postinst.sh"
# Script files may lack a trailing newline. Without this, `%preun` is glued
# onto the last line of %post and the whole %files section is swallowed —
# rpmbuild then fails with "Installed (but unpackaged) file(s) found".
echo
echo '%preun'
sed '1{/^#!/d}' "$ROOT/packaging/prerm.sh"
echo
cat <<'EOF'
%files
/usr/bin/modelrouter
/etc/systemd/system/modelrouter.service
/usr/share/modelrouter
/usr/share/doc/modelrouter/README.md
%dir /etc/modelrouter
EOF
} > "$spec"
rpmbuild --define "_topdir $top" --define "_binary_payload w9.gzdio" -bb "$spec" \
>/dev/null 2>&1 || {
echo "[core-dist] FATAL: rpmbuild failed; spec kept at $spec"; exit 1
}
local built
built=$(find "$top/RPMS" -name 'modelrouter-*.rpm' | head -1)
[ -n "$built" ] || { echo "[core-dist] FATAL: rpmbuild produced no rpm"; exit 1; }
cp "$built" "$DIST/modelrouter-${VERSION}.x86_64.rpm"
log " built $(basename "$DIST/modelrouter-${VERSION}.x86_64.rpm")"
}
# 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/modelrouter
# 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)..."
if [ -n "$NFPM_BIN" ]; then
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"
else
package_deb_fallback
package_rpm_fallback
fi
# 4) tar.gz
log "packaging tar.gz..."
TAR_DIR="$DIST/modelrouter-$VERSION-linux-amd64"
rm -rf "$TAR_DIR"; mkdir -p "$TAR_DIR"
cp "$BUILD_BIN" "$TAR_DIR/modelrouter"
# Use the tracked sanitised template, NOT the local config.yaml: that file is
# gitignored and carries the operator's real gateway_keys / upstream keys. It
# was previously copied verbatim as config.example.yaml, which shipped a real,
# working admin key (sk-gw-local-0001) to every install — and postinst.sh copies
# the example to /etc when none exists, so it ended up live in production.
cp "$ROOT/packaging/config.example.yaml" "$TAR_DIR/config.example.yaml"
cp "$ROOT/packaging/modelrouter.service" "$TAR_DIR/modelrouter.service"
# 改名迁移脚本随包分发:升级自 llmsproxy 时代的机器需要它(dry-run 默认)。
cp "$ROOT/packaging/migrate-to-modelrouter.sh" "$TAR_DIR/migrate-to-modelrouter.sh"
mkdir -p "$TAR_DIR/adapters"
cp "$ROOT"/internal/lua/adapters/*.lua "$TAR_DIR/adapters/"
# The core distribution contains only the gateway and protocol adapters.
# Optional plugins remain in the source repository, but are neither embedded in
# the binary nor copied into release archives. Operators install selected Lua
# plugins into plugin_dir separately.
tar -C "$DIST" -czf "$DIST/modelrouter-$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"/modelrouter_*.deb "$DIST"/modelrouter-*.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 " OK $f ($((sz/1024)) KB)"
done
# dpkg-deb takes ONE archive; every extra argument is read as an additional
# control-component name. With the glob unquoted this used to expand to three
# .deb files (1.5.9 / 1.6.0 / the one just built) and dpkg-deb exited 2 — under
# `set -e` that aborted the script right here, so the "done" line never printed
# and NO content verification below ever ran. The size gate above was the only
# check that ever executed. Pick the newest deb explicitly.
newest_deb=$(ls -t "$DIST"/modelrouter_*.deb 2>/dev/null | head -1 || true)
if [ -n "$newest_deb" ]; then
dpkg-deb -I "$newest_deb" 2>/dev/null | grep -E 'Package|Version' | head -2 || true
fi
# 6) the archive must actually carry what the release notes promise.
#
# A size floor alone cannot catch this: a tar.gz missing all 70KB of plugins
# still clears the 100KB threshold easily, and the seeded-on-first-boot
# behaviour hides it at runtime — the operator sees a working billing plugin and
# concludes the package is complete. Assert the file is in the archive.
log "verifying archive contents..."
TARBALL="$DIST/modelrouter-$VERSION-linux-amd64.tar.gz"
[ -f "$TARBALL" ] || { echo "[core-dist] FATAL: $TARBALL missing"; exit 1; }
listing=$(tar tzf "$TARBALL" 2>/dev/null || true)
[ -n "$listing" ] || { echo "[core-dist] FATAL: cannot list $TARBALL"; exit 1; }
for want in modelrouter config.example.yaml modelrouter.service migrate-to-modelrouter.sh; do
if ! printf '%s\n' "$listing" | grep -q "/$want\$"; then
echo "[core-dist] FATAL: archive is missing $want"
echo " (the archive must carry the binary, config template, unit and migration script)"
exit 1
fi
log " OK archive contains $want"
done
n_plugins=$(printf '%s\n' "$listing" | grep -c "/plugins/.*\.lua\$" || true)
if [ "$n_plugins" -ne 0 ]; then
echo "[core-dist] FATAL: archive unexpectedly contains $n_plugins plugins"
exit 1
fi
log " OK archive excludes optional plugins"
n_adapters=$(printf '%s\n' "$listing" | grep -c "/adapters/.*\.lua\$" || true)
if [ "$n_adapters" -lt 10 ]; then
echo "[core-dist] FATAL: archive has only $n_adapters adapters (expected >= 10)"
exit 1
fi
log " OK archive contains $n_adapters adapters"
log "done: $DIST"