Support relocatable custom install roots

This commit is contained in:
mm644706215
2026-03-20 18:05:39 +08:00
parent 68f24e6658
commit a4c92ee59a
9 changed files with 298 additions and 51 deletions

View File

@@ -62,6 +62,27 @@ jobs:
echo "=== Tarball contents (first 20 entries) ==="
tar tJf dist/*.tar.xz | head -20
- name: Verify relocatable tarball
run: |
docker run --rm --platform linux/arm64 \
-v "$PWD/dist:/dist:ro" \
alpine:3.21 sh -euxc '
apk add --no-cache xz icu-data-full tar
TARBALL=$(ls /dist/*.tar.xz | head -1)
verify_prefix() {
prefix="$1"
rm -rf "$prefix"
mkdir -p "$prefix"
tar -xJf "$TARBALL" --strip-components=1 -C "$prefix"
"$prefix/bin/node" --version
exec_path=$("$prefix/bin/node" -e "process.stdout.write(process.execPath)")
[ "$exec_path" = "$prefix/bin/node" ]
NODE_ICU_DATA="$prefix/share/icu" "$prefix/bin/npm" --version >/dev/null
}
verify_prefix /opt/openclaw/node
verify_prefix /tmp/custom-openclaw-root/openclaw/node
'
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
@@ -115,6 +136,27 @@ jobs:
echo "=== Tarball contents (first 20 entries) ==="
tar tJf dist/*.tar.xz | head -20
- name: Verify relocatable tarball
run: |
docker run --rm --platform linux/arm64 \
-v "$PWD/dist:/dist:ro" \
alpine:3.21 sh -euxc '
apk add --no-cache xz icu-data-full tar
TARBALL=$(ls /dist/*.tar.xz | head -1)
verify_prefix() {
prefix="$1"
rm -rf "$prefix"
mkdir -p "$prefix"
tar -xJf "$TARBALL" --strip-components=1 -C "$prefix"
"$prefix/bin/node" --version
exec_path=$("$prefix/bin/node" -e "process.stdout.write(process.execPath)")
[ "$exec_path" = "$prefix/bin/node" ]
NODE_ICU_DATA="$prefix/share/icu" "$prefix/bin/npm" --version >/dev/null
}
verify_prefix /opt/openclaw/node
verify_prefix /tmp/custom-openclaw-root/openclaw/node
'
- name: Upload artifact
uses: actions/upload-artifact@v4
with:

View File

@@ -58,6 +58,7 @@ define Package/$(PKG_NAME)/install
$(INSTALL_BIN) ./root/usr/bin/openclaw-env $(1)/usr/bin/openclaw-env
$(INSTALL_DIR) $(1)/usr/libexec
$(INSTALL_BIN) ./root/usr/libexec/openclaw-paths.sh $(1)/usr/libexec/openclaw-paths.sh
$(INSTALL_BIN) ./root/usr/libexec/openclaw-node.sh $(1)/usr/libexec/openclaw-node.sh
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/controller
$(INSTALL_DATA) ./luasrc/controller/openclaw.lua $(1)/usr/lib/lua/luci/controller/openclaw.lua
$(INSTALL_DIR) $(1)/usr/lib/lua/openclaw

View File

@@ -12,10 +12,11 @@
set -e
. /usr/libexec/openclaw-paths.sh
. /usr/libexec/openclaw-node.sh
# ── Node.js 版本策略 (双版本兼容) ──
# ── Node.js 版本策略 ──
# V2: 当前推荐版本,用于 OpenClaw v2026.3.11+ (要求 >= 22.16.0)
# V1: 旧版兼容,用于 OpenClaw v2026.3.8 及更早版本
# V1: 保留给显式指定旧版环境时使用,不再作为 V2 的自动回退
NODE_VERSION_V2="22.16.0"
NODE_VERSION_V1="22.15.1"
# 默认使用 V2 版本 (可通过 NODE_VERSION 环境变量覆盖)
@@ -148,23 +149,23 @@ download_node() {
# 如果已存在且版本兼容, 跳过
if [ -x "$NODE_BIN" ]; then
local current_ver
current_ver=$("$NODE_BIN" --version 2>/dev/null | sed 's/^v//')
if [ "$current_ver" = "$node_ver" ]; then
current_ver=$(oc_read_node_version "$NODE_BIN" || true)
if [ -n "$current_ver" ] && [ "$current_ver" = "$node_ver" ]; then
log_info "Node.js v${node_ver} 已安装, 跳过下载"
return 0
fi
# ARM64 musl 使用 Alpine 打包,版本号可能不完全匹配
# 只要主版本号相同即认为兼容 (如 22.15.1 vs 22.15.0)
local cur_major=$(echo "$current_ver" | cut -d. -f1)
local want_major=$(echo "$node_ver" | cut -d. -f1)
if [ "$cur_major" = "$want_major" ]; then
log_info "Node.js v${current_ver} 已安装 (兼容 v${node_ver}), 跳过下载"
if [ -n "$current_ver" ] && oc_node_version_ge "$current_ver" "$node_ver"; then
log_info "Node.js v${current_ver} 已安装 (满足 >= v${node_ver}), 跳过下载"
return 0
fi
log_warn "当前 Node.js v${current_ver}, 将更新到 v${node_ver}"
if [ -n "$current_ver" ]; then
log_warn "当前 Node.js v${current_ver} 低于要求 v${node_ver}, 将更新"
else
log_warn "检测到现有 Node.js 文件但无法运行,将重新安装"
fi
fi
# ── 构建下载 URL 列表 (按优先级排列,支持双版本回退) ──
# ── 构建下载 URL 列表 (按优先级排列) ──
local mirror_list=""
local musl_tarball="node-v${node_ver}-${node_arch}-musl.tar.xz"
local glibc_tarball="node-v${node_ver}-${node_arch}.tar.xz"
@@ -179,11 +180,6 @@ download_node() {
mirror_list="${NODE_SELF_HOST}/${musl_tarball}"
# 2) unofficial-builds (留作将来可能支持)
mirror_list="$mirror_list ${NODE_MUSL_MIRROR}/v${node_ver}/${musl_tarball}"
# 3) 回退到 V1 版本 (兼容旧环境,仅当请求 V2 时)
if [ "$node_ver" = "$NODE_VERSION_V2" ]; then
local v1_tarball="node-v${NODE_VERSION_V1}-${node_arch}-musl.tar.xz"
mirror_list="$mirror_list ${NODE_SELF_HOST}/${v1_tarball}"
fi
else
# x64 musl: unofficial-builds 提供
# 1) unofficial-builds
@@ -262,14 +258,18 @@ download_node() {
rm -f "$tmp_file"
# 验证
if [ -x "$NODE_BIN" ]; then
local installed_ver
installed_ver=$("$NODE_BIN" --version 2>/dev/null || echo "unknown")
installed_ver=$(oc_read_node_version "$NODE_BIN" || true)
if [ -n "$installed_ver" ] && oc_node_version_ge "$installed_ver" "$node_ver"; then
log_info "Node.js ${installed_ver} 安装成功"
return 0
fi
if [ -n "$installed_ver" ]; then
log_error "Node.js 版本过低: v${installed_ver} < v${node_ver}"
else
log_error "Node.js 安装验证失败"
exit 1
fi
exit 1
}
install_pnpm() {
@@ -410,9 +410,11 @@ init_openclaw() {
do_setup() {
local node_ver="$NODE_VERSION"
local installed_node_ver=""
installed_node_ver=$(oc_read_node_version "$NODE_BIN" || true)
# 检查是否已安装
if [ -x "$NODE_BIN" ] && [ -n "$(find_oc_entry)" ]; then
if [ -n "$installed_node_ver" ] && [ -n "$(find_oc_entry)" ]; then
local oc_ver=""
local oc_entry="$(find_oc_entry)"
local oc_pkg_dir="$(dirname "$oc_entry")"
@@ -422,7 +424,7 @@ do_setup() {
echo "║ ⚠️ OpenClaw 运行环境已安装,无需重复安装 ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo " Node.js: $($NODE_BIN --version 2>/dev/null)"
echo " Node.js: v${installed_node_ver}"
[ -n "$oc_ver" ] && echo " OpenClaw: v${oc_ver}"
echo ""
echo " 如需升级,请使用: openclaw-env upgrade"
@@ -465,8 +467,10 @@ do_check() {
echo ""
# Node.js
if [ -x "$NODE_BIN" ]; then
echo " Node.js: $($NODE_BIN --version 2>/dev/null)"
local installed_node_ver=""
installed_node_ver=$(oc_read_node_version "$NODE_BIN" || true)
if [ -n "$installed_node_ver" ]; then
echo " Node.js: v${installed_node_ver}"
else
echo " Node.js: 未安装"
fi
@@ -511,7 +515,9 @@ do_upgrade() {
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
if [ ! -x "$NODE_BIN" ]; then
local installed_node_ver=""
installed_node_ver=$(oc_read_node_version "$NODE_BIN" || true)
if [ -z "$installed_node_ver" ]; then
log_error "Node.js 未安装, 请先运行: openclaw-env setup"
exit 1
fi
@@ -526,7 +532,7 @@ do_upgrade() {
current_ver=$("$NODE_BIN" -e "try{console.log(require('$oc_pkg_dir/package.json').version)}catch(e){}" 2>/dev/null) || true
fi
echo " Node.js: $($NODE_BIN --version 2>/dev/null)"
echo " Node.js: v${installed_node_ver}"
[ -n "$current_ver" ] && echo " 当前版本: v${current_ver}"
echo ""
@@ -643,7 +649,9 @@ do_setup_offline() {
fi
# 检查是否已安装
if [ -x "$NODE_BIN" ] && [ -n "$(find_oc_entry)" ]; then
local installed_node_ver=""
installed_node_ver=$(oc_read_node_version "$NODE_BIN" || true)
if [ -n "$installed_node_ver" ] && [ -n "$(find_oc_entry)" ]; then
log_warn "OpenClaw 运行环境已安装"
log_warn "如需重新离线安装,请先卸载现有环境"
exit 0
@@ -680,8 +688,10 @@ do_setup_offline() {
rm -rf "$tmp_extract"
fi
if [ -x "$NODE_BIN" ]; then
log_info "Node.js $($NODE_BIN --version 2>/dev/null) 安装成功"
local offline_node_ver=""
offline_node_ver=$(oc_read_node_version "$NODE_BIN" || true)
if [ -n "$offline_node_ver" ]; then
log_info "Node.js v${offline_node_ver} 安装成功"
else
log_error "Node.js 安装失败"
exit 1

View File

@@ -0,0 +1,68 @@
#!/bin/sh
# Shared OpenClaw Node.js runtime/version helpers.
oc_normalize_node_version() {
local version="${1:-}"
local old_ifs
[ -n "$version" ] || return 1
case "$version" in
v*) version="${version#v}" ;;
esac
case "$version" in
''|*[!0-9.]*) return 1 ;;
esac
old_ifs="$IFS"
IFS=.
set -- $version
IFS="$old_ifs"
[ "$#" -eq 3 ] || return 1
for part in "$1" "$2" "$3"; do
case "$part" in
''|*[!0-9]*) return 1 ;;
esac
done
printf '%s.%s.%s\n' "$1" "$2" "$3"
}
oc_node_version_ge() {
local lhs rhs
local old_ifs
local lhs_major lhs_minor lhs_patch
local rhs_major rhs_minor rhs_patch
lhs=$(oc_normalize_node_version "${1:-}") || return 1
rhs=$(oc_normalize_node_version "${2:-}") || return 1
old_ifs="$IFS"
IFS=.
set -- $lhs
lhs_major="$1"
lhs_minor="$2"
lhs_patch="$3"
set -- $rhs
rhs_major="$1"
rhs_minor="$2"
rhs_patch="$3"
IFS="$old_ifs"
[ "$lhs_major" -gt "$rhs_major" ] && return 0
[ "$lhs_major" -lt "$rhs_major" ] && return 1
[ "$lhs_minor" -gt "$rhs_minor" ] && return 0
[ "$lhs_minor" -lt "$rhs_minor" ] && return 1
[ "$lhs_patch" -ge "$rhs_patch" ]
}
oc_read_node_version() {
local node_bin="${1:-}"
local version
[ -n "$node_bin" ] || return 1
[ -x "$node_bin" ] || return 1
version=$("$node_bin" --version 2>/dev/null) || return 1
oc_normalize_node_version "$version"
}

View File

@@ -12,14 +12,16 @@
# 1. apk 模式: 使用 Alpine apk 安装 nodejs版本受限于 Alpine 仓库
# 2. cross 模式: 从 Node.js 官方下载 glibc 版本,转换为 musl
# 使用 patchelf 修改 node 二进制的 ELF interpreter 和 rpath
# 使其直接使用打包的 musl 链接器和共享库,无需 LD_LIBRARY_PATH
# 使其使用系统 musl 动态链接器和相对 rpath可从任意安装根目录直接运行
# 这样 process.execPath 返回正确的 node 路径,子进程 fork 也能正常工作。
# 安装路径固定为 /opt/openclaw/node (与 openclaw-env 一致)。
# ============================================================================
set -e
INSTALL_PREFIX="/opt/openclaw/node"
BUILD_MODE="${BUILD_MODE:-apk}"
DEFAULT_VERIFY_PREFIX="/opt/openclaw/node"
CUSTOM_VERIFY_PREFIX="/tmp/custom-openclaw-root/openclaw/node"
SYSTEM_MUSL_LOADER="/lib/ld-musl-aarch64.so.1"
RELATIVE_RPATH='$ORIGIN/../lib'
echo "=== Node.js ARM64 musl Build ==="
echo " Target version: v${NODE_VER}"
@@ -198,10 +200,10 @@ finalize_package() {
# 用 patchelf 修改 node 二进制
echo "=== Patching ELF binary ==="
patchelf --set-interpreter "${INSTALL_PREFIX}/lib/ld-musl-aarch64.so.1" "${PKG_DIR}/bin/node"
patchelf --set-rpath "${INSTALL_PREFIX}/lib" "${PKG_DIR}/bin/node"
echo " interpreter: ${INSTALL_PREFIX}/lib/ld-musl-aarch64.so.1"
echo " rpath: ${INSTALL_PREFIX}/lib"
patchelf --set-interpreter "/lib/ld-musl-aarch64.so.1" "${PKG_DIR}/bin/node"
patchelf --set-rpath "$RELATIVE_RPATH" "${PKG_DIR}/bin/node"
echo " interpreter: ${SYSTEM_MUSL_LOADER}"
echo " rpath: ${RELATIVE_RPATH}"
# 创建 node wrapper 脚本
cat > "${PKG_DIR}/bin/node-wrapper" << 'NODEWRAPPER'
@@ -229,20 +231,27 @@ exec "${SELF_DIR}/node" "${SELF_DIR}/../lib/node_modules/npm/bin/npx-cli.js" "$@
NPXWRAPPER
chmod +x "${PKG_DIR}/bin/npm" "${PKG_DIR}/bin/npx"
# 验证
echo "=== Verification ==="
mkdir -p "${INSTALL_PREFIX}"
cp -a "${PKG_DIR}"/* "${INSTALL_PREFIX}/"
verify_prefix() {
local prefix="$1"
local label="$2"
local exec_path=""
# 设置库路径并测试
export LD_LIBRARY_PATH="${INSTALL_PREFIX}/lib"
echo "=== Verification (${label}) ==="
rm -rf "$prefix" 2>/dev/null || true
mkdir -p "$prefix"
cp -a "${PKG_DIR}"/* "${prefix}/"
"${INSTALL_PREFIX}/bin/node" --version
"${INSTALL_PREFIX}/bin/node" -e "console.log('execPath:', process.execPath)"
"${INSTALL_PREFIX}/bin/node" -e "console.log(process.arch, process.platform, process.versions.modules)"
NODE_ICU_DATA="${INSTALL_PREFIX}/share/icu" "${INSTALL_PREFIX}/bin/npm" --version 2>/dev/null || echo "npm needs ICU data"
"${prefix}/bin/node" --version
exec_path=$("${prefix}/bin/node" -e "process.stdout.write(process.execPath)")
[ "$exec_path" = "${prefix}/bin/node" ]
"${prefix}/bin/node" -e "console.log(process.arch, process.platform, process.versions.modules)"
NODE_ICU_DATA="${prefix}/share/icu" "${prefix}/bin/npm" --version >/dev/null
rm -rf "${INSTALL_PREFIX}"
rm -rf "$prefix" 2>/dev/null || true
}
verify_prefix "$DEFAULT_VERIFY_PREFIX" "default install root"
verify_prefix "$CUSTOM_VERIFY_PREFIX" "custom install root"
# 打包
echo "=== Creating tarball ==="

View File

@@ -51,10 +51,20 @@ mkdir -p "$DATA_DIR/usr/bin"
cp "$PKG_DIR/root/usr/bin/openclaw-env" "$DATA_DIR/usr/bin/"
chmod +x "$DATA_DIR/usr/bin/openclaw-env"
# libexec helpers
mkdir -p "$DATA_DIR/usr/libexec"
cp "$PKG_DIR/root/usr/libexec/openclaw-paths.sh" "$DATA_DIR/usr/libexec/"
cp "$PKG_DIR/root/usr/libexec/openclaw-node.sh" "$DATA_DIR/usr/libexec/"
chmod +x "$DATA_DIR/usr/libexec/openclaw-paths.sh" "$DATA_DIR/usr/libexec/openclaw-node.sh"
# LuCI controller
mkdir -p "$DATA_DIR/usr/lib/lua/luci/controller"
cp "$PKG_DIR/luasrc/controller/openclaw.lua" "$DATA_DIR/usr/lib/lua/luci/controller/"
# Shared Lua helpers
mkdir -p "$DATA_DIR/usr/lib/lua/openclaw"
cp "$PKG_DIR/luasrc/openclaw/paths.lua" "$DATA_DIR/usr/lib/lua/openclaw/"
# LuCI CBI
mkdir -p "$DATA_DIR/usr/lib/lua/luci/model/cbi/openclaw"
cp "$PKG_DIR/luasrc/model/cbi/openclaw/"*.lua "$DATA_DIR/usr/lib/lua/luci/model/cbi/openclaw/"

View File

@@ -54,10 +54,20 @@ install_files() {
cp "$PKG_DIR/root/usr/bin/openclaw-env" "$dest/usr/bin/"
chmod +x "$dest/usr/bin/openclaw-env"
# libexec helpers
mkdir -p "$dest/usr/libexec"
cp "$PKG_DIR/root/usr/libexec/openclaw-paths.sh" "$dest/usr/libexec/"
cp "$PKG_DIR/root/usr/libexec/openclaw-node.sh" "$dest/usr/libexec/"
chmod +x "$dest/usr/libexec/openclaw-paths.sh" "$dest/usr/libexec/openclaw-node.sh"
# LuCI controller
mkdir -p "$dest/usr/lib/lua/luci/controller"
cp "$PKG_DIR/luasrc/controller/openclaw.lua" "$dest/usr/lib/lua/luci/controller/"
# Shared Lua helpers
mkdir -p "$dest/usr/lib/lua/openclaw"
cp "$PKG_DIR/luasrc/openclaw/paths.lua" "$dest/usr/lib/lua/openclaw/"
# LuCI CBI
mkdir -p "$dest/usr/lib/lua/luci/model/cbi/openclaw"
cp "$PKG_DIR/luasrc/model/cbi/openclaw/"*.lua "$dest/usr/lib/lua/luci/model/cbi/openclaw/"

View File

@@ -0,0 +1,43 @@
#!/bin/sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
BUILD_SCRIPT="$REPO_ROOT/scripts/build-node-musl.sh"
WORKFLOW="$REPO_ROOT/.github/workflows/build-node-musl.yml"
MAKEFILE="$REPO_ROOT/Makefile"
BUILD_IPK="$REPO_ROOT/scripts/build_ipk.sh"
BUILD_RUN="$REPO_ROOT/scripts/build_run.sh"
ENV_SCRIPT="$REPO_ROOT/root/usr/bin/openclaw-env"
fail() {
echo "FAIL: $1" >&2
exit 1
}
grep -Fq 'patchelf --set-interpreter "/lib/ld-musl-aarch64.so.1"' "$BUILD_SCRIPT" || fail "build script should use system musl loader"
grep -Fq '$ORIGIN/../lib' "$BUILD_SCRIPT" || fail "build script should use relative rpath"
if grep -Fq 'patchelf --set-interpreter "${INSTALL_PREFIX}/lib/ld-musl-aarch64.so.1"' "$BUILD_SCRIPT"; then
fail "build script should not hardcode interpreter to install prefix"
fi
grep -Fq 'verify_prefix /opt/openclaw/node' "$WORKFLOW" || fail "workflow should verify default install path"
grep -Fq 'verify_prefix /tmp/custom-openclaw-root/openclaw/node' "$WORKFLOW" || fail "workflow should verify custom install path"
grep -Fq 'oc_node_version_ge "$installed_ver" "$node_ver"' "$ENV_SCRIPT" || fail "installer should enforce minimum node version after extraction"
if grep -Fq 'mirror_list="$mirror_list ${NODE_SELF_HOST}/${v1_tarball}"' "$ENV_SCRIPT"; then
fail "installer should not auto-fallback from V2 to V1 tarball"
fi
grep -Fq 'openclaw-paths.sh' "$MAKEFILE" || fail "package makefile should install path helper"
grep -Fq 'openclaw-node.sh' "$MAKEFILE" || fail "package makefile should install node helper"
grep -Fq 'openclaw/paths.lua' "$MAKEFILE" || fail "package makefile should install Lua path helper"
grep -Fq 'openclaw-paths.sh' "$BUILD_IPK" || fail "ipk builder should package path helper"
grep -Fq 'openclaw-node.sh' "$BUILD_IPK" || fail "ipk builder should package node helper"
grep -Fq 'openclaw/paths.lua' "$BUILD_IPK" || fail "ipk builder should package Lua path helper"
grep -Fq 'openclaw-paths.sh' "$BUILD_RUN" || fail "run builder should package path helper"
grep -Fq 'openclaw-node.sh' "$BUILD_RUN" || fail "run builder should package node helper"
grep -Fq 'openclaw/paths.lua' "$BUILD_RUN" || fail "run builder should package Lua path helper"
echo "ok"

View File

@@ -0,0 +1,54 @@
#!/bin/sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
. "$REPO_ROOT/root/usr/libexec/openclaw-node.sh"
fail() {
echo "FAIL: $1" >&2
exit 1
}
normalized=$(oc_normalize_node_version "v22.16.1") || fail "normalize valid version"
[ "$normalized" = "22.16.1" ] || fail "normalized version value"
if oc_normalize_node_version "broken-version" >/dev/null 2>&1; then
fail "invalid version should not normalize"
fi
oc_node_version_ge "22.16.0" "22.16.0" || fail "exact version should satisfy requirement"
oc_node_version_ge "22.16.1" "22.16.0" || fail "newer patch should satisfy requirement"
oc_node_version_ge "23.0.0" "22.16.0" || fail "newer major should satisfy requirement"
if oc_node_version_ge "22.15.1" "22.16.0"; then
fail "older minor version should not satisfy requirement"
fi
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT INT TERM
cat > "$tmpdir/node-ok" <<'EOF'
#!/bin/sh
if [ "${1:-}" = "--version" ]; then
echo "v22.16.2"
exit 0
fi
exit 1
EOF
chmod +x "$tmpdir/node-ok"
cat > "$tmpdir/node-bad" <<'EOF'
#!/bin/sh
exit 127
EOF
chmod +x "$tmpdir/node-bad"
read_ver=$(oc_read_node_version "$tmpdir/node-ok") || fail "read runnable node version"
[ "$read_ver" = "22.16.2" ] || fail "read version value"
if oc_read_node_version "$tmpdir/node-bad" >/dev/null 2>&1; then
fail "broken node binary should not be accepted"
fi
echo "ok"