mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
两个都是发布链上的真缺陷,构建 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。
252 lines
12 KiB
Bash
Executable File
252 lines
12 KiB
Bash
Executable File
#!/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" |