#!/usr/bin/env bash # # 部署脚本的**环境自足**声明:一处给全,不再每条命令旁各写一个预检。 # # # 为什么要有这个文件 # # 2026-09-14 这条链上一共修了**四次**同一形状的错,每次形态一模一样: # **部署路径上假设了一个环境变量存在**。四次都是"再加一个预检", # 而预检只挡**已知的那一个** —— 挡住 `HOME` 之后 `GOMODCACHE` 才露出来 # (两者其实是同一个根因的两面:`HOME` 决定 go 默认缓存位置)。 # pi 的原话:**"否则第五次一定会来。"** # # 四次记录(每次都是实测,不是推测): # 1. `HOME` 未设 ⇒ `go` 拒绝工作(`homeagent` 的 build.sh,当次修了一处) # 2. `HOME` 未设 ⇒ 同一族再现(`install.sh`,当次又修一处) # 3. `TMPDIR` 未设/不可写 ⇒ 构建与测试把临时文件写到意外位置 # (`redeploy-plugin.sh:160` 曾写成 `TMPDIR=${TMPDIR:-/tmp} npx tsc …`,即就地兜了一次) # 4. `GOMODCACHE`/`GOPATH` 都未设(`HOME` 也没设)⇒ # `go: module cache not found: neither GOMODCACHE nor GOPATH is set` # # # 纪律 # # · 需要环境的脚本**开头** `source` 本文件,别在命令行里内联兜底; # · 本文件只设**未设**的变量(`:-` 语义),绝不覆盖调用者已经给的值; # · 失败要**说清是环境问题**并按仓库约定用退出码 2(环境),不要让它冒充代码缺陷。 # # ⚠️ **顺序不是风格,是正确性**(pi 评审 2026-09-14): # 下面 ① → ④ 的顺序有依赖 —— ③ 用 `command -v go` 探测,而 `command -v` **走 PATH**, # 所以 ④(修 PATH)必须在 ③ **之前**。原先反了:在 ④ 要修的那个环境里(PATH 为空)、 # ③ 的探测必然失败,紧接着 ④ 把 PATH 装上,后面的步骤**又能**找到 go # —— **探测结论与实际可用性相反**。一条探测所依赖的东西,正是同一文件后面那一步要修的 # 东西:这正是本文件存在的理由(消除"部署路径上的环境假设"),所以顺序写死在这里并解释。 # # ⚠️ 本文件被 `source`,**不能带 `set -e`**(那会改调用者的语义)。 # 调用者自己已经有 `set -uo pipefail`(`install.sh` 另有 `-e`),这里不重复声明。 # shellcheck shell=bash # 记录本次兜了哪些变量(调用者可以打出来,见文件末尾的 `agentmail_env_report`)。 # 目的:**"某条脚本忘了 source" 目前没有信号** —— `AGENTMAIL_ENV_DEFAULTS` 只是被 export, # 值本身不进正常输出。谁把 `source` 删了或写错位置,输出**少了那一行**才是可见信号; # 否则"环境本来就齐"与"忘了 source"在输出上完全同形(又是"看起来在兜、其实没兜")。 AGENTMAIL_ENV_DEFAULTS="" # ④(**必须排在最前**,见文件头)PATH:只保证常见系统目录在(最小环境里 PATH 可能为空)。 case ":${PATH:-}:" in *:/usr/bin:*) : ;; *) PATH="${PATH:+$PATH:}/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" export PATH AGENTMAIL_ENV_DEFAULTS="$AGENTMAIL_ENV_DEFAULTS PATH=+系统目录" ;; esac # ① HOME:`go`/`npm`/`npx` 都会用它推默认缓存与配置位置。 # # ★ **不能无脑给 `/root`**(pi 评审 2026-09-14):`redeploy-gateway.sh` 没有 EUID 断言, # 于是在**非 root + 空 HOME** 下会拿到 `HOME=/root`,接下来的 `go build`/`npm ci` # 往 `/root/go`、`/root/.npm` 写 ⇒ **EACCES**,而那串报错看起来是**代码/工程问题** # —— 正是本文件存在的理由(把环境问题说成代码问题)。所以按身份分叉,**且验证可写**。 # ★ **`HOME` 已给也要验可写**(pi 的思路我扩了一步,实测发现的第二个口子): # 非 root 调用者若带着 `HOME=/root`(sudo -E、从 root shell 继承、容器里挂错)过来, # 只判"未设"就放行 ⇒ 后面 `go build`/`npm ci` 写 `/root/go`、`/root/.npm` ⇒ **EACCES**, # 报错看起来是代码问题 —— 与"空 HOME"是**同一个后果**,只是入参不同。 # 实测:`setpriv --reuid=65534 env -i HOME=/root` ⇒ `touch $HOME/probe` 被拒。 # 以 **root** 跑时 `/root` 写得进去,所以这条只在非 root 下触发 —— # 判据落在"**能不能写**",不落在"路径长得像不像"。 _am_uid="$(id -u 2>/dev/null || echo 0)" _am_home_given=0 if [ -z "${HOME:-}" ]; then if [ "$_am_uid" = "0" ]; then _am_home="/root" else # 非 root:从 passwd 库取该 uid 的 home(比猜 /root 或 /home/$USER 准) _am_home="$(getent passwd "$_am_uid" 2>/dev/null | cut -d: -f6 || true)" fi else _am_home_given=1 _am_home="$HOME" fi # 目录必须落在**调用者写得进去**的地方,否则就是把问题往后推。 # 注意:`mkdir -p` 对已存在的不可写目录会**返回成功**,所以必须再单独判 `-w`。 _am_fallback="${TMPDIR:-/tmp}/agentmail-home-$_am_uid" if [ -z "$_am_home" ] || [ ! -d "$_am_home" ] || [ ! -w "$_am_home" ]; then if [ "$_am_home_given" = "1" ] && [ -n "$_am_home" ]; then # 调用者显式给了 HOME 却不可写:**说清楚**再改判,不静默换目录 # (静默改 HOME 会让"东西写到哪去了"变成一个谜)。 printf ' [WARN] 调用者给的 HOME=%s 不可写(非 root 且非该用户所有);改判到 %s\n' \ "$_am_home" "$_am_fallback" >&2 fi _am_home="$_am_fallback" fi if mkdir -p "$_am_home" 2>/dev/null && [ -w "$_am_home" ]; then if [ "$_am_home" != "${HOME:-}" ]; then HOME="$_am_home" export HOME AGENTMAIL_ENV_DEFAULTS="$AGENTMAIL_ENV_DEFAULTS HOME=$HOME" fi else # 连一处可写的 HOME 都找不到 ⇒ **这是环境不足**,按约定 exit 2, # 并且用人话说明"不要把它当代码问题"(调用者的 go/npm 报错会更晦涩)。 printf '\n [FAIL] 环境不足:找不到可写的 HOME(试过 %s)\n' "$_am_home" >&2 printf ' 这是**环境问题**,不是代码问题。药方:在可写的环境里重跑,或显式给 HOME=<可写目录>。\n' >&2 exit 2 fi # ② TMPDIR:构建暂存与测试都往这里写。缺省 /tmp 可能与真实挂载不符, # 但"空的 TMPDIR"比"指错一个已存在的目录"更糟 —— 后者会让判据看错地方。 if [ -z "${TMPDIR:-}" ]; then TMPDIR=/tmp export TMPDIR AGENTMAIL_ENV_DEFAULTS="$AGENTMAIL_ENV_DEFAULTS TMPDIR=$TMPDIR" fi # ③ GOMODCACHE / GOPATH:go 的模块缓存。**只在 go 存在时**推导, # 且只在两者都空的时候推导(有 GOPATH 时 go 自己会算 GOMODCACHE)。 # # ⚠️ 这里的 `command -v` 依赖 ④ 已经跑过(PATH 就位)—— 见文件头的顺序说明。 if command -v go >/dev/null 2>&1; then if [ -z "${GOMODCACHE:-}" ] && [ -z "${GOPATH:-}" ]; then GOPATH="$HOME/go" export GOPATH AGENTMAIL_ENV_DEFAULTS="$AGENTMAIL_ENV_DEFAULTS GOPATH=$GOPATH" fi fi export AGENTMAIL_ENV_DEFAULTS="${AGENTMAIL_ENV_DEFAULTS# }" # 调用者打一行,让"兜了什么"**可见**(也是"忘了 source"的信号 —— 少了这行就不对)。 agentmail_env_report() { if [ -n "$AGENTMAIL_ENV_DEFAULTS" ]; then printf ' 环境兜底:%s\n' "$AGENTMAIL_ENV_DEFAULTS" else printf ' 环境兜底:(无 —— 调用者已提供全部)\n' fi }