mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 01:18:08 +00:00
模型与运行库是发行版能力的一部分,不做成「装完再自己下载」:
- package-linux.sh:新增 stage_multimodal_assets(),打 server/full 前校验产物
SHA256SUMS、逐文件非空、运行库架构与目标一致,缺一即失败;client 包不含。
顺带修掉三个让打包在最后一步才炸的既有缺陷:
· 版本串直接取 git describe(v1.0.0-68-gxxx-dirty)不是合法包版本——deb 要求
数字开头、rpm 不允许 '-'。以前只有显式 VERSION=1.0.3 才打得出来;默认路径
从来没通过过。现在归一化,非数字开头时显式报错。
· 三处 mktemp -d 落在 /tmp(本机 9.8GB tmpfs),而 staging 要复制 719MB 模型,
中途 ENOSPC;报错文本指向某个 .onnx 文件,看着像资产坏了。改为落在与构建产物
同盘的 build/.stage-tmp。
· 开工前删掉旧的 SHA256SUMS:失败时脚本直接退出、不重算,留着像在为残缺产物背书。
- setup.sh:把包内 /usr/lib/homeagent/models/chinese-clip-vit-b16-onnx 软链到
<dataDir>/models/…(不复制 754MB、保持 dataDir 可迁移、已有自定义目录不覆盖)
- homeagent.service:ExecStart 改 /usr/bin/homed(deb 装在那里,此前写 /usr/local/bin,
装了也不会被 unit 用上)、加 ONNXRUNTIME_DIR 与 StateDirectory、MemoryMax 2G→8G
(实测常驻约 4.5GB,2G 会在首次全量建索引时被 cgroup OOM)
- control-{full,server}:补 libstdc++6 / libgcc-s1(libonnxruntime.so 需要)
- providers/{chineseclip,qwen3vl}:findOnnxLib 支持 ONNXRUNTIME_DIR / ONNX_ML_DIR
与包内 /usr/lib/homeagent/onnxruntime,随包的运行库才真的会被用上
- postinst:修掉两个让「装完即用」失效的点——它检查 /lib/systemd/system/ 下的 unit
而 deb 装到 /etc/systemd/system/,于是 daemon-reload/enable **从未执行**;以及
setup.sh 的失败被 `|| true` 吞掉(正是 initconfig 静默缺陷被藏住的原因)。现在
三个候选路径都查、失败可见并给出补救命令、首装 start / 升级 restart。
- docs/zh/multimodal-space.md:新增「随包分发」一节,并修正播种判据的说明
验收(从真实 deb 走一遍,不是读脚本):
- 包内 initconfig 已是动态链接,凭据真的写进 config.db
- 解包 → 按 postinst 顺序跑 setup.sh → 包内 homed 冷启动:
multimodal space active: provider=chineseclip dim=512 fp=cd2a495cf990
modalities=[text image]
- 全新安装的默认值确实被播种(core.plugin.dir / provider / model_dir 都在)
- 真实对话拿到回复(3.4s,回复中含唯一标记)
- 包内模型 SHA256SUMS 5/5 通过;包内 ORT 与源同 sha256;server 包 722MB
(旧版 17MB,差额即模型与运行库);full 包同样含全部资产;client 包不含
673 lines
25 KiB
Bash
673 lines
25 KiB
Bash
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||
BUILD_DIR="${PROJECT_ROOT}/build"
|
||
DIST_DIR="${PROJECT_ROOT}/dist/linux"
|
||
VERSION="${VERSION:-$(git -C "$PROJECT_ROOT" describe --tags --dirty 2>/dev/null || echo "0.8.0")}"
|
||
|
||
# git describe 给出的是 v1.0.0-68-gba0b5a1-dirty 这类描述串,它不是合法的包版本:
|
||
# deb 的 Version 必须以数字开头,rpm 的 Version 不允许 '-'(那是版本/发布的分隔符)。
|
||
# 以前只有显式传 VERSION=1.0.3 才打得出来,默认路径一跑就死在 dpkg-deb 上——
|
||
# 而且死在 stage 之后,前面每条日志都是真的,只有最后一个产物没生成。
|
||
PKG_VERSION="${VERSION#v}"
|
||
case "$PKG_VERSION" in
|
||
[0-9]*) ;;
|
||
*) echo "ERROR: 包版本必须以数字开头(得到 '$VERSION')。请显式设置 VERSION=x.y.z 后重试。" >&2; exit 1 ;;
|
||
esac
|
||
PKG_VERSION="$(printf '%s' "$PKG_VERSION" | sed -e 's/-/+/g')"
|
||
PACKAGE_ROOT="${PROJECT_ROOT}/deploy/packaging/linux"
|
||
GO="${GO:-$(command -v go 2>/dev/null || echo "go")}"
|
||
|
||
ARCH="${1:-amd64}" # amd64 or arm64
|
||
|
||
# server/full 发行包默认带 Chinese-CLIP ONNX 产物与 ONNX Runtime。
|
||
# 二进制大资产不进 git:发布环境通过这两个目录提供已验证的产物;若缺失,
|
||
# server/full 打包必须明确失败,不能生成一个「默认启用但装完不能用」的假包。
|
||
CHINESECLIP_BUNDLE_DIR="${CHINESECLIP_BUNDLE_DIR:-$BUILD_DIR/model-assets/chinese-clip-vit-b16-onnx}"
|
||
ONNXRUNTIME_ASSET_DIR="${ONNXRUNTIME_ASSET_DIR:-$BUILD_DIR/runtime-assets/$ARCH}"
|
||
ONNXRUNTIME_LIB="${ONNXRUNTIME_LIB:-$ONNXRUNTIME_ASSET_DIR/libonnxruntime.so}"
|
||
ONNXRUNTIME_LICENSE="${ONNXRUNTIME_LICENSE:-$ONNXRUNTIME_ASSET_DIR/LICENSE}"
|
||
ONNXRUNTIME_NOTICES="${ONNXRUNTIME_NOTICES:-$ONNXRUNTIME_ASSET_DIR/ThirdPartyNotices.txt}"
|
||
|
||
# 打包 staging 会把 719MB 模型真的复制一份,临时目录必须落在构建目录所在的磁盘,
|
||
# 不能落在系统临时目录:本机 /tmp 是 9.8GB tmpfs,一次 full 包 staging 就能写满,
|
||
# 而且失败发生在 cp 进行到一半,报出来是 "No space left on device"——看上去像
|
||
# 资产/版本有问题,实际只是临时目录选错了文件系统。
|
||
STAGE_TMP="${BUILD_DIR}/.stage-tmp"
|
||
|
||
# electron 官方发布物用 x64/arm64 命名,而 Debian 用 amd64/arm64。
|
||
# 两者在 arm64 上恰好同名,amd64 上不同——此前缓存查找统一用 TAR_ARCH
|
||
# (amd64),于是 electron-v*-linux-x64.zip 永远命中不到,amd64 GUI 只能
|
||
# 靠"回退到 host node_modules"这条路组装。干净 worktree 里没有完整
|
||
# node_modules,GUI 就被静默跳过。故单独映射。
|
||
ACTION="${2:-all}" # all, build, deb, tar, rpm
|
||
|
||
DEB_ARCH="$ARCH"
|
||
RPM_ARCH="$ARCH"
|
||
TAR_ARCH="$ARCH"
|
||
case "$ARCH" in
|
||
amd64) DEB_ARCH="amd64"; RPM_ARCH="x86_64"; TAR_ARCH="amd64"; ELECTRON_ARCH="x64" ;;
|
||
arm64) DEB_ARCH="arm64"; RPM_ARCH="aarch64"; TAR_ARCH="arm64"; ELECTRON_ARCH="arm64" ;;
|
||
*) echo "Unknown arch: $ARCH (use amd64 or arm64)"; exit 1 ;;
|
||
esac
|
||
|
||
echo "=== HomeAgent Linux Packager ==="
|
||
echo "Version: $VERSION"
|
||
[ "$PKG_VERSION" = "$VERSION" ] || echo "Package: $PKG_VERSION (normalized for deb/rpm)"
|
||
echo "Arch: $ARCH"
|
||
echo ""
|
||
|
||
# ---- prepare go.mod for Linux build: point SDK replace at the in-repo SDK ----
|
||
# SDK 仓库(含工具链/示例)以嵌套 git 仓库形式维护在 third_party/homeagent-sdk,
|
||
# 打包直接用该目录,不再 clone 到 /tmp。
|
||
prepare_gomod() {
|
||
local gomod="$PROJECT_ROOT/go.mod"
|
||
local sdk_local="$PROJECT_ROOT/third_party/homeagent-sdk"
|
||
local sdk_clone="/tmp/homeagent-sdk"
|
||
local patched=0
|
||
|
||
if grep -q 'replace gitcode.com/JianFeeeee/homeagent-sdk' "$gomod"; then
|
||
echo ">>> Updating go.mod: replacing Windows SDK path with in-repo SDK..."
|
||
if [ -d "$sdk_local" ]; then
|
||
sed -i.bak "s|^replace gitcode.com/JianFeeeee/homeagent-sdk => .*|replace gitcode.com/JianFeeeee/homeagent-sdk => ${sdk_local}|" "$gomod"
|
||
patched=1
|
||
elif [ ! -d "$sdk_clone" ]; then
|
||
echo ">>> Cloning SDK to $sdk_clone (in-repo SDK missing)..."
|
||
git clone git@gitcode.com:JianFeeeee/homeagent-sdk.git "$sdk_clone" 2>/dev/null || \
|
||
git clone https://gitcode.com/JianFeeeee/homeagent-sdk.git "$sdk_clone" 2>/dev/null || true
|
||
if [ -d "$sdk_clone" ]; then
|
||
sed -i.bak "s|^replace gitcode.com/JianFeeeee/homeagent-sdk => .*|replace gitcode.com/JianFeeeee/homeagent-sdk => ${sdk_clone}|" "$gomod"
|
||
patched=1
|
||
else
|
||
echo "WARNING: Cannot clone SDK. Build may fail."
|
||
fi
|
||
else
|
||
sed -i.bak "s|^replace gitcode.com/JianFeeeee/homeagent-sdk => .*|replace gitcode.com/JianFeeeee/homeagent-sdk => ${sdk_clone}|" "$gomod"
|
||
patched=1
|
||
fi
|
||
fi
|
||
return $patched
|
||
}
|
||
|
||
restore_gomod() {
|
||
if [ -f "$PROJECT_ROOT/go.mod.bak" ]; then
|
||
echo ">>> Restoring original go.mod..."
|
||
mv "$PROJECT_ROOT/go.mod.bak" "$PROJECT_ROOT/go.mod"
|
||
fi
|
||
}
|
||
|
||
# ---- temporarily move .syso files (Windows COFF objects) for Linux builds ----
|
||
hide_syso() {
|
||
echo ">>> Hiding .syso files (Windows-only object files)..."
|
||
for dir in homed waiter; do
|
||
if [ -f "$PROJECT_ROOT/cmd/$dir/$dir.syso" ]; then
|
||
mv "$PROJECT_ROOT/cmd/$dir/$dir.syso" "$PROJECT_ROOT/cmd/$dir/$dir.syso.bak"
|
||
fi
|
||
done
|
||
}
|
||
|
||
restore_syso() {
|
||
for dir in homed waiter; do
|
||
if [ -f "$PROJECT_ROOT/cmd/$dir/$dir.syso.bak" ]; then
|
||
mv "$PROJECT_ROOT/cmd/$dir/$dir.syso.bak" "$PROJECT_ROOT/cmd/$dir/$dir.syso"
|
||
fi
|
||
done
|
||
}
|
||
|
||
# ensure both are always restored on exit
|
||
restore_all() { restore_gomod; restore_syso; rmdir "$STAGE_TMP" 2>/dev/null || true; }
|
||
trap restore_all EXIT
|
||
|
||
# ---- build Go binaries via existing build.sh ----
|
||
build_go() {
|
||
echo ">>> Building Go binaries for linux/$ARCH..."
|
||
|
||
prepare_gomod || true
|
||
hide_syso
|
||
|
||
local suffix="linux_${ARCH}"
|
||
local homed_bin="$BUILD_DIR/homed_$suffix"
|
||
local waiter_bin="$BUILD_DIR/waiter_$suffix"
|
||
local initconfig_bin="$BUILD_DIR/initconfig_$suffix"
|
||
|
||
# 先删旧产物:否则本次构建失败后,残留文件会让「产物存在」判据假绿。
|
||
rm -f "$homed_bin" "$waiter_bin" "$initconfig_bin"
|
||
|
||
bash "$PROJECT_ROOT/deploy/packaging/build.sh" "linux/$ARCH" "homed"
|
||
test -x "$homed_bin"
|
||
if ! go version -m "$homed_bin" | grep -Eq 'build[[:space:]]+-tags=.*onnxruntime'; then
|
||
echo "ERROR: homed 不是 onnxruntime 构建,拒绝打 server/full 包:$homed_bin" >&2
|
||
return 1
|
||
fi
|
||
|
||
bash "$PROJECT_ROOT/deploy/packaging/build.sh" "linux/$ARCH" "waiter"
|
||
test -x "$waiter_bin"
|
||
bash "$PROJECT_ROOT/deploy/packaging/build.sh" "linux/$ARCH" "initconfig"
|
||
test -x "$initconfig_bin"
|
||
|
||
echo " homed: $homed_bin ($(du -h "$homed_bin" | cut -f1), onnxruntime)"
|
||
echo " waiter: $waiter_bin ($(du -h "$waiter_bin" | cut -f1))"
|
||
echo " initconfig: $initconfig_bin ($(du -h "$initconfig_bin" | cut -f1))"
|
||
echo ""
|
||
}
|
||
|
||
# ---- build GUI (manual directory assembly, avoids electron-packager network issues) ----
|
||
#
|
||
# electron 运行时必须按**目标架构**取,不能用 host 的
|
||
# node_modules/electron/dist——那里永远是 host 架构(本机 x64)。
|
||
# v1.0.0 / v1.0.1 的 arm64 full/client 包都踩了这个坑:目录名带
|
||
# -arm64、homed/waiter 确实是 aarch64,但里面的 electron 是 x86-64,
|
||
# 在 arm64 机器上一启动就是 Exec format error(从未被交叉验证过)。
|
||
#
|
||
# 现在改为优先从 electron 缓存里取对应架构的 zip,并在最后做
|
||
# 一道强制校验:架构不符就删掉目录并跳过 GUI,宁可不发也不发坏包。
|
||
build_gui() {
|
||
local gui_dir="$PROJECT_ROOT/cmd/gui"
|
||
local gui_out="$BUILD_DIR/homeagent-gui-linux-${TAR_ARCH}"
|
||
|
||
if [ -d "$gui_out" ]; then
|
||
echo ">>> GUI already built at $gui_out (delete to rebuild)"
|
||
return
|
||
fi
|
||
|
||
echo ">>> Building GUI directory for linux/$ARCH..."
|
||
|
||
# 判据是 electron 包本身在不在,而不是 node_modules 目录在不在。
|
||
#
|
||
# npm install 失败(离线、网络受限)会留下一个只有一两个条目的空壳
|
||
# node_modules,目录存在但 electron 缺失。只看目录会以为"已安装",
|
||
# 于是 ever 读不到版本、缓存匹配退化、最后走到"host dist 也没有"而
|
||
# 静默跳过 GUI——包名和目录名全都正确,只是没有 GUI,没有任何一步报错。
|
||
if [ ! -f "$gui_dir/node_modules/electron/package.json" ]; then
|
||
if [ -d "$gui_dir/node_modules" ]; then
|
||
echo " node_modules 存在但 electron 缺失(疑似上次 npm install 未完成)"
|
||
fi
|
||
echo " npm install..."
|
||
if ! (cd "$gui_dir" && npm install --production); then
|
||
echo " WARNING: npm install 失败——离线环境下这是预期的。"
|
||
echo " GUI 需要 cmd/gui/node_modules/electron 或 ~/.cache/electron 缓存。"
|
||
fi
|
||
fi
|
||
|
||
# electron 版本优先从已安装的包里读,保证运行时与 app 依赖一致。
|
||
# 读不到时退而从 package.json 的依赖声明里取数字部分(它可能写成
|
||
# "^33.0.0" 这类范围,只用于给缓存匹配一个提示,匹配不上仍会走通配)。
|
||
local ever
|
||
ever=$(python3 -c "import json;print(json.load(open('$gui_dir/node_modules/electron/package.json'))['version'])" 2>/dev/null || true)
|
||
if [ -z "$ever" ]; then
|
||
ever=$(python3 -c "
|
||
import json, re
|
||
d = json.load(open('$gui_dir/package.json'))
|
||
spec = (d.get('devDependencies', {}) or {}).get('electron') or (d.get('dependencies', {}) or {}).get('electron') or ''
|
||
m = re.search(r'(\\d+(?:\\.\\d+)*)', spec)
|
||
print(m.group(1) if m else '')
|
||
" 2>/dev/null || true)
|
||
[ -n "$ever" ] && echo " electron 版本取自 package.json 依赖声明: $ever(非精确)"
|
||
fi
|
||
|
||
mkdir -p "$gui_out"
|
||
|
||
# 优先:缓存里的目标架构 zip(~/.cache/electron/<hash>/electron-v<ver>-linux-<arch>.zip)
|
||
local zip=""
|
||
if [ -n "$ever" ]; then
|
||
zip=$(find "$HOME/.cache/electron" -name "electron-v${ever}-linux-${ELECTRON_ARCH}.zip" 2>/dev/null | head -1)
|
||
fi
|
||
if [ -z "$zip" ]; then
|
||
zip=$(find "$HOME/.cache/electron" -name "electron-v*-linux-${ELECTRON_ARCH}.zip" 2>/dev/null | head -1)
|
||
fi
|
||
|
||
if [ -n "$zip" ]; then
|
||
echo " electron runtime: $(basename "$zip")"
|
||
unzip -q -o "$zip" -d "$gui_out"
|
||
else
|
||
# 回退:仅当目标架构 == host 架构时才能用 host 的 dist
|
||
local host_arch
|
||
case "$(uname -m)" in
|
||
x86_64) host_arch=amd64 ;;
|
||
aarch64|arm64) host_arch=arm64 ;;
|
||
*) host_arch=unknown ;;
|
||
esac
|
||
if [ "$TAR_ARCH" != "$host_arch" ]; then
|
||
echo " WARNING: 缺 electron-v*-linux-${ELECTRON_ARCH}.zip 缓存,且目标架构与 host"
|
||
echo " ($host_arch) 不同——不能用 host 的 electron 冒充。跳过 GUI。"
|
||
echo " 解法:下载 electron-v${ever:-<ver>}-linux-${ELECTRON_ARCH}.zip 到"
|
||
echo " ~/.cache/electron/<任意子目录>/ 后重跑。"
|
||
rm -rf "$gui_out"
|
||
return
|
||
fi
|
||
local electron_dir="$gui_dir/node_modules/electron/dist"
|
||
if [ ! -f "$electron_dir/electron" ]; then
|
||
echo " WARNING: electron binary not found at $electron_dir. GUI will be skipped."
|
||
rm -rf "$gui_out"
|
||
return
|
||
fi
|
||
echo " electron runtime: host node_modules (同架构 $host_arch)"
|
||
cp -r "$electron_dir"/* "$gui_out/" 2>/dev/null
|
||
fi
|
||
|
||
mkdir -p "$gui_out/resources/app/node_modules"
|
||
mkdir -p "$gui_out/resources/app/renderer"
|
||
rm -f "$gui_out/resources/default_app.asar" 2>/dev/null
|
||
|
||
# copy app source
|
||
cp "$gui_dir/main.js" "$gui_out/resources/app/"
|
||
cp "$gui_dir/preload.js" "$gui_out/resources/app/"
|
||
cp "$gui_dir/package.json" "$gui_out/resources/app/"
|
||
cp "$gui_dir/renderer/index.html" "$gui_out/resources/app/renderer/"
|
||
cp "$gui_dir/renderer/app.js" "$gui_out/resources/app/renderer/"
|
||
cp "$gui_dir/renderer/style.css" "$gui_out/resources/app/renderer/" 2>/dev/null || true
|
||
cp "$gui_dir/renderer/mascot.svg" "$gui_out/resources/app/renderer/" 2>/dev/null || true
|
||
|
||
# production node_modules for app
|
||
if [ -d "$gui_dir/node_modules" ]; then
|
||
for mod in icojs; do
|
||
if [ -d "$gui_dir/node_modules/$mod" ]; then
|
||
cp -r "$gui_dir/node_modules/$mod" "$gui_out/resources/app/node_modules/"
|
||
fi
|
||
done
|
||
fi
|
||
|
||
# create desktop entry and symlink
|
||
cat > "$gui_out/resources/app/homeagent-gui.desktop" << DESKTOP
|
||
[Desktop Entry]
|
||
Name=HomeAgent
|
||
Comment=HomeAgent Desktop GUI
|
||
Exec=$gui_out/homeagent-gui
|
||
Terminal=false
|
||
Type=Application
|
||
Categories=Utility;
|
||
Icon=$gui_out/resources/app/icon.svg
|
||
DESKTOP
|
||
|
||
# create launcher script
|
||
cat > "$gui_out/homeagent-gui" << 'LAUNCHER'
|
||
#!/bin/sh
|
||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
exec "$DIR/electron" "$DIR/resources/app" "$@"
|
||
LAUNCHER
|
||
chmod +x "$gui_out/homeagent-gui"
|
||
chmod +x "$gui_out/electron"
|
||
|
||
# 最后一道强制校验:electron 二进制的实际架构必须匹配目标架构。
|
||
# 不做这步就会重现 v1.0.0/v1.0.1 的隐形坏包:包名、目录名、
|
||
# homed/waiter 全对,只有 electron 是错架构,直到用户在 arm64 机器上
|
||
# 双击才发现 Exec format error。
|
||
local want_pat
|
||
case "$TAR_ARCH" in
|
||
amd64) want_pat="x86-64" ;;
|
||
arm64) want_pat="aarch64" ;;
|
||
*) want_pat="" ;;
|
||
esac
|
||
if [ -n "$want_pat" ]; then
|
||
local got
|
||
got=$(file -b "$gui_out/electron" 2>/dev/null || echo "")
|
||
if ! printf '%s' "$got" | grep -q "$want_pat"; then
|
||
echo " ERROR: electron 架构不符——期望 $want_pat,实际: ${got%%,*}"
|
||
echo " 删除 GUI 目录并跳过(宁可不发,也不发装了跑不起来的包)。"
|
||
rm -rf "$gui_out"
|
||
return
|
||
fi
|
||
fi
|
||
|
||
echo " GUI built: $gui_out ($(du -sh "$gui_out" | cut -f1), $(file -b "$gui_out/electron" | cut -d, -f2 | tr -d ' '))"
|
||
echo ""
|
||
}
|
||
|
||
# ---- stage files for a variant ----
|
||
stage_variant() {
|
||
local variant="$1"
|
||
local staging="$2"
|
||
local suffix="linux_${ARCH}"
|
||
|
||
echo ">>> Staging $variant..."
|
||
|
||
mkdir -p "$staging/usr/bin"
|
||
mkdir -p "$staging/etc/systemd/system"
|
||
mkdir -p "$staging/var/lib/homeagent"
|
||
|
||
local initconfig_bin="$BUILD_DIR/initconfig_$suffix"
|
||
|
||
case "$variant" in
|
||
full)
|
||
cp "$BUILD_DIR/homed_$suffix" "$staging/usr/bin/homed"
|
||
cp "$BUILD_DIR/waiter_$suffix" "$staging/usr/bin/waiter"
|
||
cp "$PROJECT_ROOT/deploy/homeagent.service" "$staging/etc/systemd/system/homeagent.service"
|
||
[ -f "$initconfig_bin" ] && cp "$initconfig_bin" "$staging/usr/bin/initconfig"
|
||
stage_setup "$staging"
|
||
stage_multimodal_assets "$staging"
|
||
stage_gui "$staging"
|
||
;;
|
||
server)
|
||
cp "$BUILD_DIR/homed_$suffix" "$staging/usr/bin/homed"
|
||
cp "$BUILD_DIR/waiter_$suffix" "$staging/usr/bin/waiter"
|
||
cp "$PROJECT_ROOT/deploy/homeagent.service" "$staging/etc/systemd/system/homeagent.service"
|
||
[ -f "$initconfig_bin" ] && cp "$initconfig_bin" "$staging/usr/bin/initconfig"
|
||
stage_setup "$staging"
|
||
stage_multimodal_assets "$staging"
|
||
;;
|
||
client)
|
||
cp "$BUILD_DIR/waiter_$suffix" "$staging/usr/bin/waiter"
|
||
stage_gui "$staging"
|
||
;;
|
||
esac
|
||
|
||
chmod 755 "$staging/usr/bin/"* 2>/dev/null || true
|
||
echo " OK"
|
||
}
|
||
|
||
stage_gui() {
|
||
local staging="$1"
|
||
local gui_src="$BUILD_DIR/homeagent-gui-linux-${TAR_ARCH}"
|
||
if [ -d "$gui_src" ]; then
|
||
mkdir -p "$staging/usr/lib/homeagent-gui"
|
||
cp -r "$gui_src"/* "$staging/usr/lib/homeagent-gui/"
|
||
cat > "$staging/usr/bin/homeagent-gui" << 'SCRIPT'
|
||
#!/bin/sh
|
||
exec /usr/lib/homeagent-gui/homeagent-gui "$@"
|
||
SCRIPT
|
||
chmod 755 "$staging/usr/bin/homeagent-gui"
|
||
else
|
||
echo " WARNING: GUI not built, skipping GUI staging"
|
||
fi
|
||
}
|
||
|
||
stage_setup() {
|
||
local staging="$1"
|
||
local setup_src="$PROJECT_ROOT/deploy/packaging/linux/setup.sh"
|
||
if [ -f "$setup_src" ]; then
|
||
mkdir -p "$staging/usr/lib/homeagent"
|
||
cp "$setup_src" "$staging/usr/lib/homeagent/setup.sh"
|
||
chmod 755 "$staging/usr/lib/homeagent/setup.sh"
|
||
fi
|
||
}
|
||
|
||
# server/full 的 ONNX 资产。模型与运行库是发行版能力的一部分,不是可选下载:
|
||
# 只要打 server/full 包,两者缺一就失败。client 包不运行 homed,故不携带。
|
||
stage_multimodal_assets() {
|
||
local staging="$1"
|
||
local model_dst="$staging/usr/lib/homeagent/models/chinese-clip-vit-b16-onnx"
|
||
local ort_dst="$staging/usr/lib/homeagent/onnxruntime"
|
||
local licenses="$staging/usr/share/doc/homeagent/licenses"
|
||
|
||
if [ ! -d "$CHINESECLIP_BUNDLE_DIR" ]; then
|
||
echo "ERROR: Chinese-CLIP 产物目录不存在:$CHINESECLIP_BUNDLE_DIR" >&2
|
||
echo "先运行 scripts/export_chineseclip_onnx.py,再通过 CHINESECLIP_BUNDLE_DIR 指向产物。" >&2
|
||
return 1
|
||
fi
|
||
for f in TextEncoder.onnx VisionEncoder.onnx embed_config.json vocab.txt reference.json SHA256SUMS; do
|
||
if [ ! -s "$CHINESECLIP_BUNDLE_DIR/$f" ]; then
|
||
echo "ERROR: Chinese-CLIP 产物缺少或为空:$CHINESECLIP_BUNDLE_DIR/$f" >&2
|
||
return 1
|
||
fi
|
||
done
|
||
if ! (cd "$CHINESECLIP_BUNDLE_DIR" && sha256sum -c SHA256SUMS); then
|
||
echo "ERROR: Chinese-CLIP SHA256SUMS 校验失败,拒绝打包。" >&2
|
||
return 1
|
||
fi
|
||
|
||
if [ ! -s "$ONNXRUNTIME_LIB" ]; then
|
||
echo "ERROR: ONNX Runtime 不存在:$ONNXRUNTIME_LIB" >&2
|
||
echo "通过 ONNXRUNTIME_ASSET_DIR 或 ONNXRUNTIME_LIB 指向与目标架构匹配的资产。" >&2
|
||
return 1
|
||
fi
|
||
for notice in "$ONNXRUNTIME_LICENSE" "$ONNXRUNTIME_NOTICES"; do
|
||
if [ ! -s "$notice" ]; then
|
||
echo "ERROR: ONNX Runtime 许可证资产缺失:$notice" >&2
|
||
return 1
|
||
fi
|
||
done
|
||
local runtime_desc
|
||
runtime_desc=$(file -b "$ONNXRUNTIME_LIB")
|
||
case "$ARCH" in
|
||
amd64) printf '%s' "$runtime_desc" | grep -qE 'x86-64|x86_64' || {
|
||
echo "ERROR: ONNX Runtime 架构不是 amd64:$runtime_desc" >&2; return 1; } ;;
|
||
arm64) printf '%s' "$runtime_desc" | grep -qE 'aarch64|ARM aarch64' || {
|
||
echo "ERROR: ONNX Runtime 架构不是 arm64:$runtime_desc" >&2; return 1; } ;;
|
||
esac
|
||
|
||
mkdir -p "$model_dst" "$ort_dst" "$licenses"
|
||
cp -a "$CHINESECLIP_BUNDLE_DIR/." "$model_dst/"
|
||
install -m 0755 "$ONNXRUNTIME_LIB" "$ort_dst/libonnxruntime.so"
|
||
|
||
# 许可证随二进制分发:Chinese-CLIP = Apache-2.0;ONNX Runtime = MIT,
|
||
# 同时携带其 ThirdPartyNotices(含 MKL/protobuf/zlib 等第三方条款)。
|
||
cp /usr/share/common-licenses/Apache-2.0 "$licenses/Chinese-CLIP-Apache-2.0.txt"
|
||
cp "$ONNXRUNTIME_LICENSE" "$licenses/ONNX-Runtime-MIT.txt"
|
||
cp "$ONNXRUNTIME_NOTICES" "$licenses/ONNX-Runtime-ThirdPartyNotices.txt"
|
||
cat > "$licenses/MODEL-SOURCES.txt" <<EOF
|
||
Chinese-CLIP ViT-B/16
|
||
upstream: https://huggingface.co/OFA-Sys/chinese-clip-vit-base-patch16
|
||
license: Apache-2.0
|
||
exported-by: scripts/export_chineseclip_onnx.py
|
||
dimensions: 512
|
||
modalities: text,image
|
||
|
||
ONNX Runtime
|
||
upstream: https://github.com/microsoft/onnxruntime
|
||
license: MIT (see ONNX-Runtime-MIT.txt and ONNX-Runtime-ThirdPartyNotices.txt)
|
||
EOF
|
||
|
||
echo " ONNX assets: model=$(du -sh "$model_dst" | cut -f1) runtime=$(du -h "$ort_dst/libonnxruntime.so" | cut -f1)"
|
||
}
|
||
|
||
# ---- create .deb ----
|
||
build_deb() {
|
||
local variant="$1"
|
||
local staging="$2"
|
||
local deb_dir="${DIST_DIR}/deb"
|
||
mkdir -p "$deb_dir"
|
||
|
||
local pkg_name="homeagent-${variant}_${PKG_VERSION}_${DEB_ARCH}.deb"
|
||
local deb_root
|
||
deb_root="$(mktemp -d "$STAGE_TMP/deb.XXXXXX")"
|
||
|
||
mkdir -p "$deb_root/DEBIAN"
|
||
|
||
local control_file="$PACKAGE_ROOT/deb/control-${variant}"
|
||
local installed_size_kb
|
||
installed_size_kb=$(du -sk "$staging" | cut -f1)
|
||
|
||
sed -e "s/VERSION_PLACEHOLDER/$PKG_VERSION/g" \
|
||
-e "s/ARCH_PLACEHOLDER/$DEB_ARCH/g" \
|
||
-e "s/INSTALLED_SIZE_PLACEHOLDER/$installed_size_kb/g" \
|
||
"$control_file" > "$deb_root/DEBIAN/control"
|
||
|
||
if [ -f "$PACKAGE_ROOT/deb/postinst" ]; then
|
||
cp "$PACKAGE_ROOT/deb/postinst" "$deb_root/DEBIAN/postinst"
|
||
chmod 755 "$deb_root/DEBIAN/postinst"
|
||
fi
|
||
|
||
if [ -f "$PACKAGE_ROOT/deb/prerm" ]; then
|
||
cp "$PACKAGE_ROOT/deb/prerm" "$deb_root/DEBIAN/prerm"
|
||
chmod 755 "$deb_root/DEBIAN/prerm"
|
||
fi
|
||
|
||
cp -r "$staging"/* "$deb_root/" 2>/dev/null || true
|
||
|
||
echo ">>> Building .deb: $pkg_name"
|
||
fakeroot dpkg-deb --build "$deb_root" "$deb_dir/$pkg_name" 2>/dev/null || \
|
||
dpkg-deb --build "$deb_root" "$deb_dir/$pkg_name" 2>&1
|
||
rm -rf "$deb_root"
|
||
echo " Created: $deb_dir/$pkg_name ($(du -h "$deb_dir/$pkg_name" | cut -f1))"
|
||
}
|
||
|
||
# ---- create combined .tar.gz (all binaries, no variant split) ----
|
||
build_tar() {
|
||
local tar_dir="${DIST_DIR}/tar"
|
||
mkdir -p "$tar_dir"
|
||
|
||
local archive_name="homeagent_${PKG_VERSION}_linux_${TAR_ARCH}.tar.gz"
|
||
local archive_dir="homeagent-${PKG_VERSION}-linux-${TAR_ARCH}"
|
||
|
||
# build combined staging
|
||
local staging
|
||
staging="$(mktemp -d "$STAGE_TMP/tar.XXXXXX")"
|
||
mkdir -p "$staging/usr/bin" "$staging/usr/lib/homeagent" "$staging/etc/systemd/system"
|
||
|
||
# copy all available binaries
|
||
for bin in homed waiter initconfig; do
|
||
local src="$BUILD_DIR/${bin}_linux_${TAR_ARCH}"
|
||
[ -f "$src" ] && cp "$src" "$staging/usr/bin/$bin"
|
||
done
|
||
|
||
# setup script
|
||
local setup_src="$PROJECT_ROOT/deploy/packaging/linux/setup.sh"
|
||
[ -f "$setup_src" ] && cp "$setup_src" "$staging/usr/lib/homeagent/setup.sh"
|
||
cp "$PROJECT_ROOT/deploy/homeagent.service" "$staging/etc/systemd/system/homeagent.service"
|
||
stage_multimodal_assets "$staging"
|
||
|
||
# GUI if available
|
||
local gui_src="$BUILD_DIR/homeagent-gui-linux-${TAR_ARCH}"
|
||
if [ -d "$gui_src" ]; then
|
||
mkdir -p "$staging/usr/lib/homeagent-gui"
|
||
cp -r "$gui_src"/* "$staging/usr/lib/homeagent-gui/"
|
||
cat > "$staging/usr/bin/homeagent-gui" << 'SCRIPT'
|
||
#!/bin/sh
|
||
exec /usr/lib/homeagent-gui/homeagent-gui "$@"
|
||
SCRIPT
|
||
chmod 755 "$staging/usr/bin/homeagent-gui"
|
||
fi
|
||
|
||
chmod 755 "$staging/usr/bin/"* 2>/dev/null || true
|
||
|
||
echo ">>> Building .tar.gz: $archive_name"
|
||
(cd "$staging" && tar czf "$tar_dir/$archive_name" --transform "s|^\.|${archive_dir}|" .)
|
||
echo " Created: $tar_dir/$archive_name ($(du -h "$tar_dir/$archive_name" | cut -f1))"
|
||
rm -rf "$staging"
|
||
}
|
||
|
||
# ---- create .rpm (via fpm if available) ----
|
||
build_rpm() {
|
||
local variant="$1"
|
||
local staging="$2"
|
||
local rpm_dir="${DIST_DIR}/rpm"
|
||
mkdir -p "$rpm_dir"
|
||
|
||
local pkg_name="homeagent-${variant}-${PKG_VERSION}-1.${RPM_ARCH}.rpm"
|
||
|
||
# find fpm
|
||
local fpm_bin="$(command -v fpm 2>/dev/null || true)"
|
||
if [ -z "$fpm_bin" ]; then
|
||
fpm_bin="$(find /home -name "fpm" -type f -path "*/bin/*" 2>/dev/null | head -1 || true)"
|
||
fi
|
||
|
||
if [ -z "$fpm_bin" ]; then
|
||
echo " SKIP .rpm: fpm not installed. Install it with: gem install fpm"
|
||
echo " Alternatively, build RPM on Fedora/RHEL using:"
|
||
echo " rpmbuild -ba deploy/packaging/linux/homeagent.spec"
|
||
return
|
||
fi
|
||
|
||
# find or extract rpmbuild (fpm needs it)
|
||
local rpmbuild_dir="/tmp/rpmext"
|
||
if [ ! -f "$rpmbuild_dir/usr/bin/rpmbuild" ]; then
|
||
# try to extract from cached deb packages
|
||
local rpm_deb
|
||
rpm_deb="$(find /tmp -name "rpm_*.deb" -type f 2>/dev/null | head -1)"
|
||
if [ -z "$rpm_deb" ]; then
|
||
rpm_deb="$(find "$PROJECT_ROOT" -name "rpm_*.deb" -type f 2>/dev/null | head -1)"
|
||
fi
|
||
if [ -n "$rpm_deb" ]; then
|
||
mkdir -p "$rpmbuild_dir"
|
||
(cd "$rpmbuild_dir" && ar x "$rpm_deb" 2>/dev/null && \
|
||
tar --no-same-permissions -xf data.tar.zst --zstd 2>/dev/null) || true
|
||
for libdeb in /tmp/librpm*.deb; do
|
||
[ -f "$libdeb" ] && (cd "$rpmbuild_dir" && ar x "$libdeb" 2>/dev/null && \
|
||
tar --no-same-permissions -xf data.tar.zst --zstd 2>/dev/null) || true
|
||
done
|
||
fi
|
||
fi
|
||
|
||
if [ -f "$rpmbuild_dir/usr/bin/rpmbuild" ]; then
|
||
export PATH="$rpmbuild_dir/usr/bin:$PATH"
|
||
export LD_LIBRARY_PATH="$rpmbuild_dir/usr/lib/x86_64-linux-gnu${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||
export RPM_CONFIGDIR="$rpmbuild_dir/usr/lib/rpm"
|
||
fi
|
||
|
||
echo ">>> Building .rpm via fpm: $pkg_name"
|
||
"$fpm_bin" -s dir -t rpm \
|
||
-n "homeagent-${variant}" \
|
||
-v "$VERSION" \
|
||
--iteration 1 \
|
||
-a "$RPM_ARCH" \
|
||
--description "HomeAgent ${variant^} package" \
|
||
--url "https://github.com/trueagent/HomeAgent" \
|
||
--license "Proprietary" \
|
||
-C "$staging" \
|
||
-p "$rpm_dir/$pkg_name" \
|
||
. 2>&1
|
||
echo " Created: $rpm_dir/$pkg_name"
|
||
}
|
||
|
||
# ---- main ----
|
||
main() {
|
||
local target_arch="$ARCH"
|
||
|
||
mkdir -p "$BUILD_DIR" "$STAGE_TMP"
|
||
|
||
case "$ACTION" in
|
||
all|build)
|
||
build_go
|
||
build_gui
|
||
;;
|
||
esac
|
||
|
||
if [ "$ACTION" = "build" ]; then
|
||
echo ""
|
||
echo "=== Build complete. Binaries in $BUILD_DIR ==="
|
||
exit 0
|
||
fi
|
||
|
||
mkdir -p "$DIST_DIR"
|
||
|
||
# 上次成功构建留下的校验和必须在本次开工前删掉:本次若中途失败,脚本直接退出、
|
||
# 不重算 SHA256SUMS,旧的它会一直躺在 dist 里,看上去像在为这一批残缺产物背书。
|
||
rm -f "$DIST_DIR/SHA256SUMS"
|
||
|
||
for variant in full server client; do
|
||
echo ""
|
||
echo "=============================================="
|
||
echo " Packaging: $variant"
|
||
echo "=============================================="
|
||
|
||
local staging
|
||
staging=$(mktemp -d "$STAGE_TMP/stage.XXXXXX")
|
||
stage_variant "$variant" "$staging"
|
||
|
||
case "$ACTION" in
|
||
all|deb) build_deb "$variant" "$staging" ;;
|
||
esac
|
||
|
||
case "$ACTION" in
|
||
all|rpm) build_rpm "$variant" "$staging" ;;
|
||
esac
|
||
|
||
rm -rf "$staging"
|
||
done
|
||
|
||
case "$ACTION" in
|
||
all|tar) build_tar ;;
|
||
esac
|
||
|
||
echo ""
|
||
echo "=== Done! Packages in: $DIST_DIR ==="
|
||
echo ""
|
||
echo "Summary:"
|
||
mapfile -t release_files < <(find "$DIST_DIR" -type f \( -name "*.deb" -o -name "homeagent_*.tar.gz" -o -name "*.rpm" \) 2>/dev/null | sort)
|
||
for f in "${release_files[@]}"; do
|
||
echo " $(du -h "$f" | cut -f1) $f"
|
||
done
|
||
# 全部包生成之后一次计算,避免边打边算漏掉后生成的产物。
|
||
if [ ${#release_files[@]} -gt 0 ]; then
|
||
(
|
||
cd "$DIST_DIR"
|
||
find . -type f \( -name "*.deb" -o -name "homeagent_*.tar.gz" -o -name "*.rpm" \) \
|
||
-print0 | sort -z | xargs -0 sha256sum > SHA256SUMS
|
||
)
|
||
echo " SHA256SUMS: $DIST_DIR/SHA256SUMS"
|
||
fi
|
||
}
|
||
|
||
main
|