#!/usr/bin/env bash # 构建 homeagent-mail-bridge —— 用**明确指定的** SDK 源码,并记录"这次到底用了哪份代码"。 # # # 这份脚本被改过一轮,因为它的第一版判错了根因(pi 2026-09-14 晚逐条反驳) # # 第一版的错:把 `go version -m <内核>` 里的 # dep ... homeagent-sdk v0.9.2 / => ./third_party/homeagent-sdk (devel) # 当成"内核链的是 0.9.x 血脉",然后按候选根目录**第一个命中就算**去解析 SDK 源码, # 命中的是 `/root/ha-test/third_party/homeagent-sdk`(C-ABI 时代的老 checkout,无 plugin.bin 支持)。 # 而本机共有 6+ 份同名 checkout,内核自己用的是 1.3.0 那份。两个教训: # # ① **不要用"第一个存在的路径"当权威来源** —— 猜根目录会命中不相关的 checkout, # "两条血脉"于是成了脚本的产物,而不是事实; # ② **不要把模块版本字符串当身份** —— `replace => local (devel)` 时那个版本号 # 只是 require 行的文本残留,不代表实际编译的代码。 # # 所以现在:SDK 源码**只认显式指定**(`HOMEAGENT_SDK_DIR`)。 # 不猜、不试探、不"第一个命中"。要指定内核的构建目录就显式传进来。 # # # 不变量是什么 # # 不是"版本号相等"(那正是第一版想抛弃的东西),而是 **wire 协议 + 一次真实握手**: # 产物要能被内核 `protocol=2` 认下来。这个脚本能做的是"把身份记清楚 + 把明显不对的挡下来", # 真握手由内核启动时完成(所以部署后要回看一次日志,见末尾提示)。 set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SDK_MODULE="gitcode.com/JianFeeeee/homeagent-sdk" KERNEL_BIN="${HOMEAGENT_KERNEL_BIN:-/usr/local/bin/homed}" die() { echo "构建失败:$*" >&2; exit 1; } # cron/非登录 shell 里 HOME 可能没设,Go 会因此拒绝工作(曾经在这里栽过) export HOME="${HOME:-/root}" export GOPATH="${GOPATH:-$HOME/go}" export GOCACHE="${GOCACHE:-${TMPDIR:-/tmp}/go-build-agentmail}" mkdir -p "$GOCACHE" # ── 1. SDK 源码:只认显式指定 ─────────────────────────────────────── [[ -n "${HOMEAGENT_SDK_DIR:-}" ]] || die "必须显式指定 SDK 源码树:HOMEAGENT_SDK_DIR=/path/to/homeagent-sdk 为什么不让脚本自己猜:本机有 6+ 份同名 checkout(有的是 C-ABI 时代的老代码), 第一版按候选根目录'第一个命中就算',命中了 /root/ha-test 那份老 checkout, 于是把'内核链老血脉'这个**错误结论**当成了事实写进注释。猜路径 = 制造事实。" SDK_DIR="$(cd "$HOMEAGENT_SDK_DIR" && pwd)" [[ -f "$SDK_DIR/go.mod" ]] || die "$SDK_DIR 不是 Go 模块(没有 go.mod)" # 身份用 **realpath + 内容哈希**,不用版本字符串(版本号可能是 -ldflags 注入的默认值, # 也可能是注释里的示例 —— 后者我在 electron 侧的判据里刚踩过一次)。 SDK_HASH="$(find "$SDK_DIR" -type f -name '*.go' -print0 2>/dev/null | sort -z | xargs -0 cat 2>/dev/null | sha256sum | cut -c1-16)" SDK_VER="$(sed 's://.*::' "$SDK_DIR/meta/meta.go" 2>/dev/null | grep -m1 -oE 'Version = "[0-9]+\.[0-9]+\.[0-9]+"' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true)" echo "[build] SDK 源码:$SDK_DIR" echo "[build] meta.Version(已剥注释)= ${SDK_VER:-未知} | 内容哈希 = ${SDK_HASH:-未知}" # 内核签名的**提示**(不是判据):按模块名精确联接,不按"输出里第一个 =>"。 # "两张表各缺一半时必须按 id 联接"—— 读 go version -m 的输出同理。 if [[ -x "$KERNEL_BIN" ]]; then KERNEL_INFO="$(go version -m "$KERNEL_BIN" 2>/dev/null || true)" KERNEL_SDK_LINE="$(awk -v m="$SDK_MODULE" '$1=="dep" && $2==m {print NR": "$0}' <<<"$KERNEL_INFO" | head -1)" if [[ -n "$KERNEL_SDK_LINE" ]]; then KERNEL_SDK_DEP="$(cut -d' ' -f3 <<<"${KERNEL_SDK_LINE#*: }")" # 二进制本身会变(实测:两次读数之间它被重编过),所以"现在链的是什么"必须带上 # (路径, mtime, 正在运行的进程启动时间) —— 否则那份读数没有时间坐标(pi 2026-09-14)。 KERNEL_MTIME="$(stat -c '%y' "$KERNEL_BIN" 2>/dev/null || echo '?')" KERNEL_PID_START="$(for d in /proc/[0-9]*; do c="$(tr '\0' ' ' < "$d/cmdline" 2>/dev/null || true)" case "$c" in *homed*) stat -c 'pid=%n 启动=%y' "$d" 2>/dev/null; break;; esac done)" echo "[build] 内核二进制:$KERNEL_BIN mtime=$KERNEL_MTIME;运行中的进程:${KERNEL_PID_START:-(没在跑)}" # 只接受**紧跟在 SDK 那一行之后**的 => 行(按位置配对的唯一正确写法) KERNEL_SDK_REPL="$(awk -v m="$SDK_MODULE" ' $1=="dep" && $2==m {want=NR+1} want && NR==want && $1=="=>" {print $2; exit}' <<<"$KERNEL_INFO")" echo "[build] 内核 $KERNEL_BIN 的 dep 行:$KERNEL_SDK_DEP${KERNEL_SDK_REPL:+(本地替换 $KERNEL_SDK_REPL)}" echo "[build] 注意:这条只是**提示**,不是判据 —— replace 到本地目录时这个版本号只是 require 行的残留," echo "[build] 而且它指向的相对路径未必是本机这份源码。要对齐请显式传 HOMEAGENT_SDK_DIR。" fi fi # ── 2. 用临时 modfile 替换,不改动受版本管理的 go.mod ─────────────── cd "$HERE" TMP_MOD="$(mktemp -d)/go.local.mod" cp go.mod "$TMP_MOD" go mod edit -modfile="$TMP_MOD" -replace "$SDK_MODULE=$SDK_DIR" export GOFLAGS="${GOFLAGS:-} -modfile=$TMP_MOD" # 注意:这个插件**没有** `func main()`(入口由 hmapdev 生成/包装), # 所以 `go build ./...` 会报 "function main is undeclared" —— 那是正常的, # 不能用它来数 API 兼容性错误;真正的编译在 hmapdev 那一步。 # 这条工具会**重写 plugin.json/plg.json**(实测:只把文件末尾的换行吃掉了)。 # 构建不该把源码树弄脏 —— 在 electron 那边我们刚为同一形状定过规矩 # (脏树产物不得自称发布候选)。所以:先记哈希,构建后若被改就还原并出声。 MANIFESTS=(plugin.json plg.json) SNAP_DIR="$(mktemp -d)" for f in "${MANIFESTS[@]}"; do [[ -f "$f" ]] && cp "$f" "$SNAP_DIR/$f"; done hmapdev build || die "hmapdev build 失败(编译错误见上;它才是真正的编译入口)" for f in "${MANIFESTS[@]}"; do [[ -f "$SNAP_DIR/$f" ]] || continue if ! cmp -s "$SNAP_DIR/$f" "$f"; then cp "$SNAP_DIR/$f" "$f" echo "[build] 注意:hmapdev 改写了 $f(已还原)—— 构建工具不该动受版本管理的源码;" echo "[build] 否则每次构建都把树弄脏,而'脏树'在别的端已经是'不是发布候选'的判据了。" fi done rm -rf "$SNAP_DIR" # ── 3. 产物自报身份 ───────────────────────────────────────────────── BIN="build/plugin.bin" [[ -f "$BIN" ]] || die "没找到产物 $BIN" echo "[build] 产物:$BIN($(stat -c%s "$BIN") 字节)" echo "[build] 本次身份:SDK=$SDK_DIR hash=$SDK_HASH version=${SDK_VER:-未知}" # 原始证据与结论放在一起(pi 2026-09-14):这场争论的全部内容就是 # "`dep` 行与 `=>` 行该怎么读",把两行原文贴进来,下一个人不必重演。 cat > build/BUILD_INFO.txt < vX.Y.Z\` 后面若跟着 \`=> (devel)\`,那个 vX.Y.Z 只是 require 行 # 的**文本残留**,不代表实际编译用的代码;本次的 SDK 源码以上面的 sdkDir/sdkHash 为准。 # · \`=>\` 必须按**模块名联接**读取(先定位 SDK 的 dep 行,只接受紧跟其后那条), # 不能取"整份输出里第一个 =>"。 EOF echo "[build] 已写 build/BUILD_INFO.txt —— 部署后请回看内核日志确认握手:" echo " grep -a 'mail-bridge' /home/newqqagent/log/homed_*.log | tail -5" echo " (判据不是版本号相等,而是 protocol=2 建链成功、且没有崩溃重启循环)"